diff --git a/forked-daapd.conf b/forked-daapd.conf index 7b2962bf..a6035bcb 100644 --- a/forked-daapd.conf +++ b/forked-daapd.conf @@ -25,9 +25,24 @@ library { # Directories to index directories = { "/srv/music" } - # Directories containing compilations - # Matches anywhere in the path (not a regexp, though) -# compilations = { "/compilations/" } + + # Directories containing podcasts + # For each directory that is indexed the path is matched against these + # names. If there is a match all items in the directory are marked as + # podcasts. Eg. if you index /srv/music, and your podcasts are in + # /srv/music/Podcasts, you can set this to "/Podcasts". + podcasts = { "/Podcasts" } + + # Directories containing compilations (eg soundtracks) + # For each directory that is indexed the path is matched against these + # names. If there is a match all items in the directory are marked as + # compilations. + compilations = { "/Compilations" } + + # Compilations usually have many artists, and if you don't want every + # artist to be listed when artist browsing in Remote, you can set + # a single name which will be used for all music in the compilation dir + compilation_artist = "Various artists" # Artwork file names (without file type extension) # forked-daapd will look for jpg and png files with these base names diff --git a/src/conffile.c b/src/conffile.c index d38fe8d0..dedb7d28 100644 --- a/src/conffile.c +++ b/src/conffile.c @@ -60,7 +60,9 @@ static cfg_opt_t sec_library[] = CFG_INT("port", 3689, CFGF_NONE), CFG_STR("password", NULL, CFGF_NONE), CFG_STR_LIST("directories", NULL, CFGF_NONE), + CFG_STR_LIST("podcasts", NULL, CFGF_NONE), CFG_STR_LIST("compilations", NULL, CFGF_NONE), + CFG_STR("compilation_artist", NULL, CFGF_NONE), CFG_STR_LIST("artwork_basenames", "{artwork,cover,Folder}", CFGF_NONE), CFG_STR_LIST("filetypes_ignore", "{.db,.ini}", CFGF_NONE), CFG_BOOL("itunes_overrides", cfg_false, CFGF_NONE), diff --git a/src/daap_query.gperf b/src/daap_query.gperf index 92d08918..fb61d635 100644 --- a/src/daap_query.gperf +++ b/src/daap_query.gperf @@ -35,6 +35,7 @@ struct dmap_query_field_map; "daap.songtime", "f.song_length", 1 "daap.songtrackcount", "f.total_tracks", 1 "daap.songtracknumber", "f.track", 1 +"daap.songuserplaycount", "f.play_count", 1 "daap.songyear", "f.year", 1 "com.apple.itunes.mediakind", "f.media_kind", 1 "com.apple.itunes.extended-media-kind", "f.media_kind", 1 diff --git a/src/db.c b/src/db.c index 8a36a48e..99c414d0 100644 --- a/src/db.c +++ b/src/db.c @@ -4122,9 +4122,12 @@ db_perthread_deinit(void) "INSERT INTO playlists (id, title, type, query, db_timestamp, path, idx, special_id)" \ " VALUES(4, 'TV Shows', 1, 'f.media_kind = 64', 0, '', 0, 5);" +#define Q_PL5 \ + "INSERT INTO playlists (id, title, type, query, db_timestamp, path, idx, special_id)" \ + " VALUES(5, 'Podcasts', 1, 'f.media_kind = 4', 0, '', 0, 1);" + /* These are the remaining automatically-created iTunes playlists, but * their query is unknown - " VALUES(5, 'Podcasts', 0, 'media_kind = 128 ', 0, '', 0, 1);" " VALUES(6, 'iTunes U', 0, 'media_kind = 256', 0, '', 0, 13);" " VALUES(7, 'Audiobooks', 0, 'media_kind = 512', 0, '', 0, 7);" " VALUES(8, 'Purchased', 0, 'media_kind = 1024', 0, '', 0, 8);" @@ -4177,6 +4180,7 @@ static const struct db_init_query db_init_queries[] = { Q_PL2, "create default smart playlist 'Music'" }, { Q_PL3, "create default smart playlist 'Movies'" }, { Q_PL4, "create default smart playlist 'TV Shows'" }, + { Q_PL5, "create default smart playlist 'Podcasts'" }, { Q_SCVER, "set schema version" }, }; diff --git a/src/dmap_fields.gperf b/src/dmap_fields.gperf index 999c9ffb..61e95412 100644 --- a/src/dmap_fields.gperf +++ b/src/dmap_fields.gperf @@ -79,6 +79,7 @@ static const struct dmap_field_map dfm_dmap_aseq = { -1, static const struct dmap_field_map dfm_dmap_asfm = { dbmfi_offsetof(type), -1, -1 }; static const struct dmap_field_map dfm_dmap_asgn = { dbmfi_offsetof(genre), -1, -1 }; static const struct dmap_field_map dfm_dmap_asdt = { dbmfi_offsetof(description), -1, -1 }; +static const struct dmap_field_map dfm_dmap_aspc = { dbmfi_offsetof(play_count), -1, -1 }; static const struct dmap_field_map dfm_dmap_asrv = { -1, -1, -1 }; static const struct dmap_field_map dfm_dmap_assr = { dbmfi_offsetof(samplerate), -1, -1 }; static const struct dmap_field_map dfm_dmap_assz = { dbmfi_offsetof(file_size), -1, -1 }; @@ -198,6 +199,7 @@ struct dmap_field; "daap.songgenre", "asgn", &dfm_dmap_asgn, DMAP_TYPE_STRING "daap.songkeywords", "asky", &dfm_dmap_asky, DMAP_TYPE_STRING "daap.songlongcontentdescription", "aslc", &dfm_dmap_aslc, DMAP_TYPE_STRING +"daap.songuserplaycount", "aspc", &dfm_dmap_aspc, DMAP_TYPE_UINT "daap.songrelativevolume", "asrv", &dfm_dmap_asrv, DMAP_TYPE_BYTE "daap.sortartist", "assa", &dfm_dmap_assa, DMAP_TYPE_STRING "daap.sortcomposer", "assc", &dfm_dmap_assc, DMAP_TYPE_STRING diff --git a/src/filescanner.c b/src/filescanner.c index 946faa1b..17f339d5 100644 --- a/src/filescanner.c +++ b/src/filescanner.c @@ -174,9 +174,11 @@ normalize_fixup_tag(char **tag, char *src_tag) static void fixup_tags(struct media_file_info *mfi) { + cfg_t *lib; size_t len; char *tag; char *sep = " - "; + char *ca; if (mfi->genre && (strlen(mfi->genre) == 0)) { @@ -290,16 +292,36 @@ fixup_tags(struct media_file_info *mfi) normalize_fixup_tag(&mfi->album_sort, mfi->album); normalize_fixup_tag(&mfi->title_sort, mfi->title); - /* If we don't have an album_artist, set it to artist */ - if (!mfi->album_artist) + /* We need to set album_artist according to media type and config */ + if (mfi->compilation) /* Compilation */ { - if (mfi->compilation) + lib = cfg_getsec(cfg, "library"); + ca = cfg_getstr(lib, "compilation_artist"); + if (ca && mfi->album_artist) + { + free(mfi->album_artist); + mfi->album_artist = strdup(ca); + } + else if (ca && !mfi->album_artist) + { + mfi->album_artist = strdup(ca); + } + else if (!ca && !mfi->album_artist) { mfi->album_artist = strdup(""); mfi->album_artist_sort = strdup(""); } - else - mfi->album_artist = strdup(mfi->artist); + } + else if (mfi->media_kind == 4) /* Podcast */ + { + if (mfi->album_artist) + free(mfi->album_artist); + mfi->album_artist = strdup(""); + mfi->album_artist_sort = strdup(""); + } + else if (!mfi->album_artist) /* Regular media without album_artist */ + { + mfi->album_artist = strdup(mfi->artist); } if (!mfi->album_artist_sort && (strcmp(mfi->album_artist, mfi->artist) == 0)) @@ -314,7 +336,7 @@ fixup_tags(struct media_file_info *mfi) void -process_media_file(char *file, time_t mtime, off_t size, int compilation, int url, struct extinf_ctx *extinf) +process_media_file(char *file, time_t mtime, off_t size, int type, struct extinf_ctx *extinf) { struct media_file_info mfi; char *filename; @@ -391,7 +413,7 @@ process_media_file(char *file, time_t mtime, off_t size, int compilation, int ur mfi.time_modified = mtime; mfi.file_size = size; - if (!url) + if (!(type & F_SCAN_TYPE_URL)) { mfi.data_kind = 0; /* real file */ ret = scan_metadata_ffmpeg(file, &mfi); @@ -415,7 +437,10 @@ process_media_file(char *file, time_t mtime, off_t size, int compilation, int ur goto out; } - mfi.compilation = compilation; + if (type & F_SCAN_TYPE_COMPILATION) + mfi.compilation = 1; + if (type & F_SCAN_TYPE_PODCAST) + mfi.media_kind = 4; /* podcast */ if (!mfi.item_kind) mfi.item_kind = 2; /* music */ @@ -509,7 +534,7 @@ process_deferred_playlists(void) /* Thread: scan */ static void -process_file(char *file, time_t mtime, off_t size, int compilation, int flags) +process_file(char *file, time_t mtime, off_t size, int type, int flags) { char *ext; @@ -538,9 +563,28 @@ process_file(char *file, time_t mtime, off_t size, int compilation, int flags) } /* Not any kind of special file, so let's see if it's a media file */ - process_media_file(file, mtime, size, compilation, 0, NULL); + process_media_file(file, mtime, size, type, NULL); } +/* Thread: scan */ +static int +check_podcast(char *path) +{ + cfg_t *lib; + int ndirs; + int i; + + lib = cfg_getsec(cfg, "library"); + ndirs = cfg_size(lib, "podcasts"); + + for (i = 0; i < ndirs; i++) + { + if (strstr(path, cfg_getnstr(lib, "podcasts", i))) + return 1; + } + + return 0; +} /* Thread: scan */ static int @@ -577,7 +621,7 @@ process_directory(char *path, int flags) #if defined(__FreeBSD__) || defined(__FreeBSD_kernel__) struct kevent kev; #endif - int compilation; + int type; int ret; if (flags & F_SCAN_BULK) @@ -608,8 +652,12 @@ process_directory(char *path, int flags) return; } - /* Check for a compilation directory */ - compilation = check_compilation(path); + /* Check if compilation and/or podcast directory */ + type = 0; + if (check_compilation(path)) + type |= F_SCAN_TYPE_COMPILATION; + if (check_podcast(path)) + type |= F_SCAN_TYPE_PODCAST; for (;;) { @@ -673,7 +721,7 @@ process_directory(char *path, int flags) } if (S_ISREG(sb.st_mode)) - process_file(entry, sb.st_mtime, sb.st_size, compilation, flags); + process_file(entry, sb.st_mtime, sb.st_size, type, flags); else if (S_ISDIR(sb.st_mode)) push_dir(&dirstack, entry); else @@ -986,7 +1034,7 @@ process_inotify_file(struct watch_info *wi, char *path, struct inotify_event *ie struct stat sb; char *deref = NULL; char *file = path; - int compilation; + int type; int ret; DPRINTF(E_DBG, L_SCAN, "File event: 0x%x, cookie 0x%x, wd %d\n", ie->mask, ie->cookie, wi->wd); @@ -1059,9 +1107,13 @@ process_inotify_file(struct watch_info *wi, char *path, struct inotify_event *ie } } - compilation = check_compilation(path); + type = 0; + if (check_compilation(path)) + type |= F_SCAN_TYPE_COMPILATION; + if (check_podcast(path)) + type |= F_SCAN_TYPE_PODCAST; - process_file(file, sb.st_mtime, sb.st_size, compilation, 0); + process_file(file, sb.st_mtime, sb.st_size, type, 0); if (deref) free(deref); diff --git a/src/filescanner.h b/src/filescanner.h index da4d78e7..8f2bf753 100644 --- a/src/filescanner.h +++ b/src/filescanner.h @@ -4,6 +4,10 @@ #include "db.h" +#define F_SCAN_TYPE_PODCAST (1 << 0) +#define F_SCAN_TYPE_COMPILATION (1 << 1) +#define F_SCAN_TYPE_URL (1 << 2) + int filescanner_init(void); @@ -18,7 +22,7 @@ struct extinf_ctx }; void -process_media_file(char *file, time_t mtime, off_t size, int compilation, int url, struct extinf_ctx *extinf); +process_media_file(char *file, time_t mtime, off_t size, int type, struct extinf_ctx *extinf); /* Actual scanners */ int diff --git a/src/filescanner_itunes.c b/src/filescanner_itunes.c index 8764adf3..46a727fb 100644 --- a/src/filescanner_itunes.c +++ b/src/filescanner_itunes.c @@ -419,6 +419,13 @@ process_track_file(plist_t trk) } } + /* Set media_kind to 4 (Podcast) if Podcast is true */ + ret = get_dictval_bool_from_key(trk, "Podcast", &boolean); + if ((ret == 0) && boolean) + { + mfi->media_kind = 4; + } + /* Don't let album_artist set to "Unknown artist" if we've * filled artist from the iTunes data in the meantime */ diff --git a/src/filescanner_m3u.c b/src/filescanner_m3u.c index 76c18e7a..e18bf329 100644 --- a/src/filescanner_m3u.c +++ b/src/filescanner_m3u.c @@ -212,7 +212,7 @@ scan_m3u_playlist(char *file, time_t mtime) if (extinf.found) DPRINTF(E_INFO, L_SCAN, "Playlist has EXTINF metadata, artist is '%s', title is '%s'\n", extinf.artist, extinf.title); - process_media_file(filename, mtime, 0, 0, 1, &extinf); + process_media_file(filename, mtime, 0, F_SCAN_TYPE_URL, &extinf); } /* Regular file */ else diff --git a/src/httpd_dacp.c b/src/httpd_dacp.c index 65710c4e..97e17a76 100644 --- a/src/httpd_dacp.c +++ b/src/httpd_dacp.c @@ -841,7 +841,9 @@ dacp_reply_playspec(struct evhttp_request *req, struct evbuffer *evbuf, char **u int ret; /* /ctrl-int/1/playspec?database-spec='dmap.persistentid:0x1'&container-spec='dmap.persistentid:0x5'&container-item-spec='dmap.containeritemid:0x9' - * With our DAAP implementation, container-spec is the playlist ID and container-item-spec is the song ID + * or (Apple Remote when playing a Podcast) + * /ctrl-int/1/playspec?database-spec='dmap.persistentid:0x1'&container-spec='dmap.persistentid:0x5'&item-spec='dmap.itemid:0x9' + * With our DAAP implementation, container-spec is the playlist ID and container-item-spec/item-spec is the song ID */ s = daap_session_find(req, query, evbuf); @@ -881,9 +883,11 @@ dacp_reply_playspec(struct evhttp_request *req, struct evbuffer *evbuf, char **u { /* Start song ID */ param = evhttp_find_header(query, "container-item-spec"); + if (!param) + param = evhttp_find_header(query, "item-spec"); if (!param) { - DPRINTF(E_LOG, L_DACP, "No container-item-spec in playspec request\n"); + DPRINTF(E_LOG, L_DACP, "No container-item-spec/item-spec in playspec request\n"); goto out_fail; } @@ -891,7 +895,7 @@ dacp_reply_playspec(struct evhttp_request *req, struct evbuffer *evbuf, char **u param = strchr(param, ':'); if (!param) { - DPRINTF(E_LOG, L_DACP, "Malformed container-item-spec parameter in playspec request\n"); + DPRINTF(E_LOG, L_DACP, "Malformed container-item-spec/item-spec parameter in playspec request\n"); goto out_fail; } @@ -900,7 +904,7 @@ dacp_reply_playspec(struct evhttp_request *req, struct evbuffer *evbuf, char **u ret = safe_hextou32(param, &id); if (ret < 0) { - DPRINTF(E_LOG, L_DACP, "Couldn't convert container-item-spec to an integer in playspec (%s)\n", param); + DPRINTF(E_LOG, L_DACP, "Couldn't convert container-item-spec/item-spec to an integer in playspec (%s)\n", param); goto out_fail; } diff --git a/src/player.c b/src/player.c index bd1cdb35..de2e20e2 100644 --- a/src/player.c +++ b/src/player.c @@ -1148,6 +1148,8 @@ source_check(void) { i++; + db_file_inc_playcount((int)cur_playing->id); + /* Stop playback if: * - at end of playlist (NULL) * - repeat OFF and at end of playlist (wraparound)