diff --git a/LICENSE b/LICENSE index 58c0cabfa3967b7afdb598d73c15bf5b58c3eb82..bc67651debd1e0fef3081a674d0cbd9655d6a7c5 100644 --- a/LICENSE +++ b/LICENSE @@ -10,4 +10,4 @@ FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF -THIS SOFTWARE. \ No newline at end of file +THIS SOFTWARE. diff --git a/README.md b/README.md index 8008095b418f4c4d55f58b325bd62ff0a0deba27..f8e7088d1ad5e7a9ac11a413f4b7b43715984f7e 100644 --- a/README.md +++ b/README.md @@ -135,4 +135,4 @@ the `path` property. ## MPD Lektor is almost MPD compatible, at least it uses the same protocol, which can -be got [here](https://www.musicpd.org/doc/html/protocol.html#). +be gotten [here](https://www.musicpd.org/doc/html/protocol.html#). diff --git a/doc/lkt.1 b/doc/lkt.1 index 3976f00b303da4ea953a2cefa5ad59793a4207c1..fd8e1648ea56b3786598cd1dad0fa640639280fc 100644 --- a/doc/lkt.1 +++ b/doc/lkt.1 @@ -118,7 +118,7 @@ Replace the queue with the content of a playlist. Keep the playling state .PD 0 .TP .PD -\fBsearch get\fP <query> +\fBsearch database\fP <query> Search and prints the kara that correspond to the query in the database .TP \fBsearch plt\fP <plt-name> <query> @@ -128,6 +128,9 @@ the query \fBsearch count\fP <query> Search and prints the number of karas that match the query .TP +\fBsearch get\fP <id> +Get informations about a specific id +.TP \fBsearch queue\fP <query> Search in the queue and prints the karas that match the query .PP diff --git a/inc/common/common.h b/inc/common/common.h index df31e41154d27d2b3824f6790289f0ec5e5f978f..faf50a377f20c390952afe7294d52e3ede268505 100644 --- a/inc/common/common.h +++ b/inc/common/common.h @@ -4,6 +4,11 @@ #include <unistd.h> #include <stdint.h> +/* May be usefull (gcc, see something else for clang): + __FLOAT_WORD_ORDER__ + __ORDER_LITTLE_ENDIAN__ + __ORDER_BIG_ENDIAN__ */ + #define not_implemented() __not_implemented(__func__,__FILE__,__LINE__) extern void __not_implemented(const char *func, char *file, int line); @@ -27,6 +32,14 @@ uint32_t be_uint32_t(const uint8_t bytes[], size_t n); Restriction: n <= 8 */ uint64_t be_uint64_t(const uint8_t bytes[], size_t n); +/* Same as `be_uint64_t` but for Big-endian doubles. + Restriction: bytes must be of length 8. */ +double be_double_t(const uint8_t bytes[]); + +/* Same as `be_double_t` but for floats. Restriction: bytes must + be of length 4. */ +float be_float_t(const uint8_t bytes[]); + /* Trim the string `str` of the char `c` from the right and the left. The string may not be null terminated. Returns a pointer to the the trimmed string diff --git a/inc/lektor/database.h b/inc/lektor/database.h index b6fe3bf2aa4333f7223d80d612ef99b1849be357..3c46b47351981ce1ebb791bad495a58f1d1e9dc1 100644 --- a/inc/lektor/database.h +++ b/inc/lektor/database.h @@ -122,6 +122,7 @@ bool database_search_queue_init (volatile sqlite3 *db, struct lkt_search *ret) bool database_search_sticker_init (volatile sqlite3 *db, struct lkt_search *ret); bool database_search_playlist_init(volatile sqlite3 *db, struct lkt_search *ret); bool database_search_iter(struct lkt_search *item); +bool database_kara_by_id(volatile sqlite3 *db, int id, struct kara_metadata *kara, char filepath[PATH_MAX]); /* Next and prev operation on the queue. */ bool database_queue_next(volatile sqlite3 *db, char filepath[PATH_MAX]); diff --git a/inc/lektor/mkv.h b/inc/lektor/mkv.h index 5474ee6c1d117ac644d4fe800275baafd409fbcb..c2d4efd074ca09032a1b235a8f59b1ab90d536a1 100644 --- a/inc/lektor/mkv.h +++ b/inc/lektor/mkv.h @@ -47,6 +47,10 @@ struct kara { Returns 0 on success and -1 on error. */ int kara_metadata_read(struct kara_metadata *dst, const char *filename); +/* Reads the duration of the .mkv file `filename`. Returns 0 on success + and -1 on error. */ +int kara_read_length(double *len, const char *filename); + /* Write metadata to a mkv file. Returns 0 on success and -1 on error */ int kara_metadata_write(struct kara_metadata *mdt, const char *filename, const char *mkvpropedit); diff --git a/src/bufferfd.c b/src/bufferfd.c index 62010a0fe365e600687c3e0c73c58dd97d75a632..9904c65234b394f0861fa152f3544a1e4af17970 100644 --- a/src/bufferfd.c +++ b/src/bufferfd.c @@ -2,7 +2,6 @@ #include <string.h> #include <unistd.h> - #include <common/bufferfd.h> static inline ssize_t @@ -40,17 +39,15 @@ bufferfd_byte(struct bufferfd *bf, uint8_t *res) return bufferfd_bytes(bf, 1, res); } -static inline ssize_t -__skip(struct bufferfd *bf, const size_t n, const size_t b_size) -{ - uint8_t b[b_size]; - return bufferfd_bytes(bf, n, b); -} - ssize_t bufferfd_skip(struct bufferfd *bf, const size_t n) { - return __skip(bf, n, n > BUFFER_MAX ? BUFFER_MAX : n); + if (lseek(bf->fd, n - bf->len + bf->pos, SEEK_CUR) < 0) + return -1; + /* The call to lseek invalidated bf->buffer */ + bf->len = 0; + __fill(bf); + return 0; } ssize_t @@ -58,8 +55,7 @@ bufferfd_seek(struct bufferfd *bf, size_t pos) { if (lseek(bf->fd, pos, SEEK_SET) < 0) return -1; - - // The call to lseek invalidated bf->buffer. + /* The call to lseek invalidated bf->buffer */ bf->pos = 0; bf->len = 0; return 0; diff --git a/src/commands.c b/src/commands.c index 497786cee00542d7dac11879057c1949d7179be9..5b32c96822f13739745be2b82b5bbba58e135b72 100644 --- a/src/commands.c +++ b/src/commands.c @@ -433,6 +433,43 @@ lkt_callback_send_row_v2(struct lkt_state *srv, size_t c, int id, int id_len, co return true; } +static bool +command_findid(struct lkt_state *srv, size_t c, char *id_str) +{ + double duration; + struct kara_metadata kara = {0}; + char filepath[PATH_MAX] = {0}; + long id = strtol(id_str, NULL, 0); /* XXX: Must be valid here */ + if (!database_kara_by_id(srv->db, id, &kara, filepath)) { + LOG_ERROR("COMMAND", "Can't get a kara with id %ld", id); + return false; + } + if (kara_read_length(&duration, filepath)) { + LOG_WARN("COMMAND", "Can't get duration of kara %ld", id); + duration = 0.; + } + int s = duration * 10e-10; + int h = s / 3600; + s = s % 3600; + int m = s / 60; + s = s % 60; + struct lkt_message *out = lkt_message_new(); + out->data_len = safe_snprintf(out->data, LKT_MESSAGE_MAX, + "Title: %s\n" + "Author: %s\n" + "Source: %s\n" + "Type: %s%d\n" + "Category: %s\n" + "Language: %s\n" + "Duration: %d:%02d:%02d\n", + kara.song_name, kara.author_name, + kara.source_name, kara.song_type, + kara.song_number, kara.category, + kara.language, h, m, s); + lkt_state_send(srv, c, out); + return true; +} + bool command_find(struct lkt_state *srv, size_t c, char *args[LKT_MESSAGE_ARGS_MAX], long continuation, bool(*init)(volatile sqlite3 *, struct lkt_search *)) @@ -449,8 +486,13 @@ command_find(struct lkt_state *srv, size_t c, char *args[LKT_MESSAGE_ARGS_MAX], .plt_name = args[0], /* In case of playlist searchs */ }; - /* Check args */ RETURN_UNLESS(args[0], "Invalid argument", false); + + /* Just an id */ + if (!args[1] && (count = strtol(args[0], NULL, 0))) + return command_findid(srv, c, args[0]); + + /* With an URI */ if (!lkt_uri_from_list(&search.ka_uri, args)) { /* Try from idx 1, in case of playlust searches */ LOG_DEBUG("COMMAND", "URI may not starts at idx 0, may be because " @@ -459,9 +501,7 @@ command_find(struct lkt_state *srv, size_t c, char *args[LKT_MESSAGE_ARGS_MAX], "Failed to create the uri", false); } - /* Make the search langand do the right action */ RETURN_UNLESS(init(srv->db, &search), "Failed to init search", false); - for (count = 0; database_search_iter(&search); ++count) continue; @@ -469,7 +509,6 @@ command_find(struct lkt_state *srv, size_t c, char *args[LKT_MESSAGE_ARGS_MAX], lkt_set_continuation(srv, c, continuation + count); else LOG_WARN("COMMAND", "%s", "Nothing found"); - lkt_uri_free(&search.ka_uri); return true; } diff --git a/src/common.c b/src/common.c index bb330464876adaa5e3129c024891f2734482178b..59f748dae69d3226254c97030fceda7375a45fba 100644 --- a/src/common.c +++ b/src/common.c @@ -33,6 +33,17 @@ be_uint32_t(const uint8_t bytes[], size_t n) return res; } +float +be_float_t(const uint8_t bytes[]) +{ + union { + uint32_t _uint; + float _float; + } ret; + ret._uint = be_uint32_t(bytes, 4); + return ret._float; +} + uint64_t be_uint64_t(const uint8_t bytes[], size_t n) { @@ -42,6 +53,17 @@ be_uint64_t(const uint8_t bytes[], size_t n) return res; } +double +be_double_t(const uint8_t bytes[]) +{ + union { + uint64_t _uint; + double _double; + } ret; + ret._uint = be_uint64_t(bytes, 8); + return ret._double; +} + char * trim(char *str, size_t len, char c) { diff --git a/src/database/find.c b/src/database/find.c index c8abc0febb018e14d56e176763f6c0f196599803..7e2f5befe2b926367b3c40f0d5ab28fa538de4ab 100644 --- a/src/database/find.c +++ b/src/database/find.c @@ -185,3 +185,33 @@ end: sqlite3_finalize(item->stmt); return false; } + +bool +database_kara_by_id(volatile sqlite3 *db, int id, struct kara_metadata *res, + char filepath[PATH_MAX]) +{ + static const char *SQL = + "SELECT song_name, source_name, category, language, author_name, " + "song_type, song_number, file_path FROM kara WHERE id = ?;"; + sqlite3_stmt *stmt = 0; + int ret = false; + + SQLITE_PREPARE(db, stmt, SQL, error); + SQLITE_BIND_INT(db, stmt, 1, id, error); + SQLITE_STEP_ROW(db, stmt, error); + if (!res) + goto no_metadata; + strncpy(res->song_name, sqlite3_column_chars(stmt, 0), LEKTOR_TAG_MAX - 1); + strncpy(res->source_name, sqlite3_column_chars(stmt, 1), LEKTOR_TAG_MAX - 1); + strncpy(res->category, sqlite3_column_chars(stmt, 2), LEKTOR_TAG_MAX - 1); + strncpy(res->language, sqlite3_column_chars(stmt, 3), LEKTOR_TAG_MAX - 1); + strncpy(res->author_name, sqlite3_column_chars(stmt, 4), LEKTOR_TAG_MAX - 1); + strncpy(res->song_type, sqlite3_column_chars(stmt, 5), LEKTOR_TAG_MAX - 1); + res->song_number = sqlite3_column_int(stmt, 6); +no_metadata: + strncpy(filepath, sqlite3_column_chars(stmt, 7), PATH_MAX - 1); + ret = true; +error: + sqlite3_finalize(stmt); + return ret; +} diff --git a/src/main/lkt.c b/src/main/lkt.c index 6057953f84154b01486ddeb97d9d04ad75354885..3999db0fd3f3a3ff3d08e2e6f397be5acbbe2372 100644 --- a/src/main/lkt.c +++ b/src/main/lkt.c @@ -842,6 +842,26 @@ redo: } } +noreturn void +search_get__(struct lkt_cmd_args *args) +{ + fail_if(args->argc != 1, "Invalid number of arguments"); + fail_if(!strtol(args->argv[0], NULL, 0), "Invalid id"); + char buff[LKT_MESSAGE_MAX]; + FILE *sock = lkt_connect(); + + write_socket(sock, "find %s\n", args->argv[0]); + for (;;) { + memset(buff, 0, LKT_MESSAGE_MAX * sizeof(char)); + read_socket(sock, buff, LKT_MESSAGE_MAX - 1); + if (STR_NMATCH(buff, "OK", 2)) + exit(EXIT_SUCCESS); + else if (STR_NMATCH(buff, "ACK", 3)) + exit(EXIT_FAILURE); + write(1, buff, strlen(buff)); + } +} + noreturn void search_plt__(struct lkt_cmd_args *args) { @@ -880,7 +900,7 @@ redo: #define search_with_cmd(func, cmd) /* I don't want to write always the same things */ \ noreturn void func (struct lkt_cmd_args *args) { search_with_cmd__(args, #cmd); } -search_with_cmd(search_get__, search) +search_with_cmd(search_db__, search) search_with_cmd(search_count__, count) search_with_cmd(search_queue__, playlistfind) #undef search_with_cmd @@ -910,6 +930,7 @@ static struct lkt_cmd_opt options_plt[] = { }; static struct lkt_cmd_opt options_search[] = { + { .name = "database", .call = search_db__ }, { .name = "get", .call = search_get__ }, { .name = "plt", .call = search_plt__ }, { .name = "count", .call = search_count__ }, diff --git a/src/mkv/mkv.c b/src/mkv/mkv.c index d1384bc1d1888609c5b165f95dff1cf7e071d550..a108cdcddbb1322c353c6f1666c7b224130fbe70 100644 --- a/src/mkv/mkv.c +++ b/src/mkv/mkv.c @@ -7,6 +7,7 @@ #include <string.h> #include <strings.h> #include <unistd.h> +#include <errno.h> #include <common/bufferfd.h> #include <lektor/mkv.h> @@ -14,23 +15,31 @@ #define MKV_TAG_MAX 64 -#define EBML_ROOT 0x1a45dfa3 -#define EBML_MKV_SEGMENT 0x18538067 -#define EBML_MKV_SEEKHEAD 0x114d9b74 -#define EBML_MKV_SEEK 0x00004dbb -#define EBML_MKV_SEEK_ID 0x000053ab -#define EBML_MKV_SEEK_POS 0x000053ac -#define EBML_MKV_TAGS 0x1254c367 -#define EBML_MKV_TAG 0x00007373 -#define EBML_MKV_TAG_TARGETS 0x000063c0 -#define EBML_MKV_TAG_TTV 0x000068ca /* TargetTypeValue */ -#define EBML_MKV_TAG_SIMPLE 0x000067c8 /* SimpleTag */ -#define EBML_MKV_TAG_NAME 0x000045a3 -#define EBML_MKV_TAG_STRING 0x00004487 -#define EBML_MKV_TAG_BINARY 0x00004485 - -#define EBML_MKV_CRC32 0x0000000bf /* CRC-32, they skip it in mpv (demux/ebml.c:463 aprox) */ -#define EBML_MKV_VOID 0x0000000ec /* VOID element */ +#define EBML_ROOT 0x1a45dfa3 +#define EBML_MKV_SEGMENT 0x18538067 +#define EBML_MKV_SEEKHEAD 0x114d9b74 +#define EBML_MKV_SEEK 0x00004dbb +#define EBML_MKV_SEEK_ID 0x000053ab +#define EBML_MKV_SEEK_POS 0x000053ac +#define EBML_MKV_TAGS 0x1254c367 +#define EBML_MKV_TAG 0x00007373 +#define EBML_MKV_TAG_TARGETS 0x000063c0 +#define EBML_MKV_TAG_TTV 0x000068ca /* Tag: TargetTypeValue, L4 */ +#define EBML_MKV_TAG_SIMPLE 0x000067c8 /* Tag: SimpleTag, L3+ */ +#define EBML_MKV_TAG_NAME 0x000045a3 +#define EBML_MKV_TAG_STRING 0x00004487 +#define EBML_MKV_TAG_BINARY 0x00004485 +#define EBML_MKV_INFO 0x1549a966 /* Segment: Info L1 */ +#define EBML_MKV_SEG_DURATION 0x00004489 /* Segment: Duration L2 */ +#define EBML_MKV_SEG_TS_SCALE 0x002ad7b1 /* Segment: TimestampScale L2 */ +#define EBML_MKV_SEG_NAME 0x00007ba9 /* Segment: Title L2 */ +#define EBML_MKV_ATTACHEMENT 0x1941a469 +#define EBML_MKV_ATTCH_NAME 0x0000466e /* Attachement: FileName, L3 */ +#define EBML_MKV_ATTCH_DESC 0x0000467e /* Attachement: FileDescription, L3 */ +#define EBML_MKV_ATTCH_MIME 0x00004660 /* Attachement: FileMimeType, L3 */ + +#define EBML_MKV_CRC32 0x0000000bf /* CRC-32, they skip it in mpv (demux/ebml.c:463 aprox) */ +#define EBML_MKV_VOID 0x0000000ec /* VOID element */ /* mkv_read_* functions take data from the bufferfd and parse the next "*" (one * of the following): @@ -122,6 +131,36 @@ mkv_read_uint(struct bufferfd *bf, uint64_t *res) return n + (ssize_t) data_len; } +/* From https://www.matroska.org/technical/specs/index.html: + Float - Big-endian, defined for 4 and 8 octets (32, 64 bits). + Here we always return a double. */ +static ssize_t +mkv_read_float(struct bufferfd *bf, double *res) +{ + uint8_t data[8]; + uint64_t data_len; + ssize_t n; + + if ((n = mkv_read_element_length(bf, &data_len)) < 0 || 8 < data_len) + return -1; + + if (data_len == 0) + *res = 0.; + else { + if (bufferfd_bytes(bf, data_len, data) < 0) + return -1; + + if (data_len == 8) + *res = be_double_t(data); + else if (data_len == 4) + *res = (double) be_float_t(data); + else + return -1; + } + + return n + (ssize_t) data_len; +} + /* static ssize_t mkv_read_binary(struct bufferfd * bf, size_t max, uint8_t * res) { @@ -394,7 +433,7 @@ mkv_read_tag(struct bufferfd *bf, struct mkv_tag *res) } static ssize_t -mkv_seek_tags(struct bufferfd *bf, size_t header_len) +mkv_seek(struct bufferfd *bf, size_t header_len, size_t what) { uint64_t seekhead_len; @@ -423,7 +462,7 @@ mkv_seek_tags(struct bufferfd *bf, size_t header_len) seekhead_len -= n; - if (eid == EBML_MKV_SEEK && s.id == EBML_MKV_TAGS) + if (eid == EBML_MKV_SEEK && s.id == what) return bufferfd_seek(bf, s.pos + header_len); } @@ -490,17 +529,110 @@ kara_read_tags(struct bufferfd *bf, struct kara_metadata *dst) return 0; } +static int +kara_read_segment_info(struct bufferfd *bf, double *len) +{ + uint64_t data_len; + uint64_t scale = 1; + + if (mkv_read_element_length(bf, &data_len) < 0) + return -1; + + while (data_len > 0) { + ssize_t n; + uint32_t eid; + + if ((n = mkv_read_element_id(bf, &eid)) < 0) + return -1; + + data_len -= n; + + if (eid == EBML_MKV_SEG_DURATION) { + if ((n = mkv_read_float(bf, len)) < 0) + return -1; + } + + else if (eid == EBML_MKV_SEG_TS_SCALE) { + if ((n = mkv_read_uint(bf, &scale)) < 0) + return -1; + } + + else if ((n = mkv_skip_element(bf)) < 0) + return -1; + + data_len -= n; + } + + *len *= scale; /* In ns */ + return 0; +} + +int +kara_read_length(double *len, const char *filename) +{ + errno = 0; + int status_code = -1; + struct bufferfd file = { + .len = 0, + .pos = 0, + .fd = open(filename, O_RDONLY), + }; + + if (file.fd < 0) { + LOG_ERROR("MKV", "Faield to open file '%s': %s", + filename, strerror(errno)); + return -1; + } + + uint32_t eid; + uint64_t elength; + ssize_t header_len = mkv_skip_header(&file); + + if (header_len < 0) + goto error; + + *len = 0.; + + for (;;) { + if (mkv_read_element_id(&file, &eid) < 0) + goto error; + + if (eid == EBML_MKV_SEEKHEAD) { + if (mkv_seek(&file, (size_t) header_len, + (size_t) EBML_MKV_SEGMENT) < 0) + goto error; + } else if (eid == EBML_MKV_INFO) { + if (kara_read_segment_info(&file, len) < 0) + goto error; + break; + } else { + if (mkv_read_element_length(&file, &elength) < 0) + goto error; + if (bufferfd_skip(&file, elength) < 0) { + goto error; + } + } + } + + status_code = 0; +error: + close(file.fd); + return status_code; +} + int kara_metadata_read(struct kara_metadata *dst, const char *filename) { int status_code = -1; struct bufferfd file; + errno = 0; file.len = 0; file.pos = 0; file.fd = open(filename, O_RDONLY); if (file.fd < 0) { - perror("Failed to open file"); + LOG_ERROR("MKV", "Failed to open file '%s': %s", + filename, strerror(errno)); return -1; } @@ -518,7 +650,7 @@ kara_metadata_read(struct kara_metadata *dst, const char *filename) goto error; if (eid == EBML_MKV_SEEKHEAD) { - if (mkv_seek_tags(&file, (size_t) header_len) < 0) + if (mkv_seek(&file, (size_t) header_len, (size_t) EBML_MKV_TAGS) < 0) goto error; } else if (eid == EBML_MKV_TAGS) { if (kara_read_tags(&file, dst) < 0)