/* * playlist.c: Media player playlist * * See the main source file 'xineliboutput.c' for copyright information and * how to reach the author. * * $Id: playlist.c,v 1.24 2009-10-31 19:59:50 phintuka Exp $ * */ #include "../features.h" #include #ifdef HAVE_LIBEXTRACTOR # include // libextractor 0.5.20 (2008-03-20) adds support for track numbers # if EXTRACTOR_VERSION < 0x00052000 # warning libextractor version too old (0.5.20 required for track numbers) # undef HAVE_LIBEXTRACTOR # endif #endif #include #include #include #include "../config.h" #include "playlist.h" #include "../logdefs.h" #ifndef PLAYLIST_CACHE # define PLAYLIST_CACHE ".xineliboutput-playlist.pls" #endif #define MAX_PLAYLIST_FILES 1024 static void strip_extension(cString& fname) { const char *ext = strrchr(fname, '.'); if (ext) fname.Truncate(ext - fname); } // // cPlaylistItem // cPlaylistItem::cPlaylistItem(const char *filename) { const char *pt; Filename = filename; Position = -1; if(NULL != (pt = strrchr(filename, '/'))) Title = pt + 1; else Title = filename; strip_extension(Title); } cPlaylistItem::cPlaylistItem(const char *filename, const char *path, const char *title, int position) { if(path[strlen(path)-1] != '/') Filename = cString::sprintf("%s/%s", path, filename); else Filename = cString::sprintf("%s%s", path, filename); Position = position; Title = title ?: filename; if (!title) strip_extension(Title); } int cPlaylistItem::Compare(const cListObject &ListObject) const { ///< Must return 0 if this object is equal to ListObject, a positive value ///< if it is "greater", and a negative value if it is "smaller". const cPlaylistItem *o = (cPlaylistItem *)&ListObject; // Use Position (if defined in playlist file) // compare as unsigned --> -1 goes to last position if(Position != o->Position) return ((unsigned int)Position) > ((unsigned int)o->Position) ? 1 : -1; // same position (or no positions definend) -> alphabetical order #if 0 return strcmp(Title, o->Title); #else // use filename, because: // - implicit playlist has no track names available when sorting // (track names are read during playback), so track name is // just file name without path. // using full path allows sorting of each album and tracks inside albums... // - "normal" playlist is ordered using Position, // so track names are never compared anyway ... return strcmp(Filename, o->Filename); #endif } // // cID3Scanner // #ifndef HAVE_LIBEXTRACTOR static const char *shell_escape(char *buf, int buflen, const cString& src, char ch) { const char *pt = *src; int n = 0; if(pt) { while(*pt && n < buflen-2) { if(*pt == ch || *pt == '\\' /*|| *pt == '\"' || *pt == '\''*/) { buf[n++] = '\\'; } buf[n++] = *pt++; } buf[n] = 0; return buf; } return ""; } #endif class cID3Scanner : public cThread { public: cPlaylist& m_List; cID3Scanner(cPlaylist& List) : cThread("Metadata scanner"), m_List(List), m_Done(false) {}; void CancelScanner(void) { Cancel(3); } private: bool m_Done; virtual void Action(void) { cPlaylistItem *Item = NULL; unsigned int Version = 0; const int priority = 10; errno = 0; if((nice(priority) == -1) && errno) LOGDBG("ID3Scanner: Can't nice to value: %d", priority); LOGDBG("ID3Scanner Started"); while(Running()) { cMutexLock ml(&m_List.m_Lock); if(Version < m_List.m_Version) { // restart after sort, add, del Item = NULL; Version = m_List.m_Version; } if(!(Item = m_List.Next(Item))) break; if(xc.IsAudioFile(Item->Filename)) { LOGDBG("Scanning metainfo for file %s", *Item->Filename); #ifdef HAVE_LIBEXTRACTOR EXTRACTOR_ExtractorList * plugins; EXTRACTOR_KeywordList * md_list; plugins = EXTRACTOR_loadDefaultLibraries(); md_list = EXTRACTOR_getKeywords(plugins, *Item->Filename); const char *key; while(md_list) { if ((key=EXTRACTOR_getKeywordTypeAsString(md_list->keywordType))) { if (!strcasecmp(key,"title")) Item->Title = md_list->keyword; else if (!strcasecmp(key,"artist")) Item->Artist = md_list->keyword; else if (!strcasecmp(key,"album")) Item->Album = md_list->keyword; else if (!strcasecmp(key,"track number")) Item->Tracknumber = cString::sprintf("%s%s", strlen(md_list->keyword) == 1 ? "0" : "", md_list->keyword); md_list=md_list->next; } } EXTRACTOR_freeKeywords(md_list); EXTRACTOR_removeAll(plugins); /* unload plugins */ #else char buf[4096]; cString Cmd = ""; if(!strcasecmp((Item->Filename) + strlen(Item->Filename) - 5, ".flac")) Cmd = cString::sprintf("metaflac " " --show-tag=TITLE " " --show-tag=ALBUM " " --show-tag=ARTIST " " --show-tag=TRACKNUMBER " " \"%s\"", shell_escape(buf, sizeof(buf)-1, Item->Filename, '\"')); else Cmd = cString::sprintf("mp3info -p \"" "ARTIST=%%a\\r\\n" "ALBUM=%%l\\r\\n" "TITLE=%%t\\r\\n" "TRACKNUMBER=%%n\\r\\n\"" " \"%s\"", shell_escape(buf, sizeof(buf)-1, Item->Filename, '\"')); cPipe p; if(p.Open(*Cmd, "r")) { cReadLine r; char *pt; while(NULL != (pt = r.Read(p))) { if(!strncasecmp(pt, "ARTIST=", 7) && strlen(pt) > 8) Item->Artist = (pt+7); else if(!strncasecmp(pt, "ALBUM=", 6) && strlen(pt) > 7) Item->Album = (pt+6); else if(!strncasecmp(pt, "TITLE=", 6) && strlen(pt) > 7) Item->Title = (pt+6); else if(!strncasecmp(pt, "TRACKNUMBER=", 12) && strlen(pt) > 12) Item->Tracknumber = cString::sprintf("%s%s", strlen(pt) == 13 ? "0" : "", (pt+12)); } } #endif } } LOGDBG("ID3Scanner Done."); m_List.PlaylistChanged(Item); m_Done = true; } }; // // cPlaylistReader // class cPlaylistReader { private: cPlaylist& m_Playlist; protected: cString m_Title; int m_Position; cPlaylistItem *Prev(void) { return m_Playlist.Last(); } public: cPlaylistReader(cPlaylist& Playlist) : m_Playlist(Playlist) {} virtual ~cPlaylistReader() {} virtual char *Parse(char *line) = 0; void ResetCache(void) { m_Title = NULL; m_Position = -1; } const char *Title(void) { return m_Title; } int Position(void) { return m_Position; } }; class cM3uReader : public cPlaylistReader { public: cM3uReader(cPlaylist& Playlist) : cPlaylistReader(Playlist), m_Next(1) {} protected: int m_Next; virtual char *Parse(char *line) { if(!*line) return NULL; if(*line == '#') { if(!strncmp(line, "#EXTINF:", 8)) { int len = -1; sscanf(line+8,"%d", &len); while(*line && *line != ',') line++; m_Title = *line ? (line+1) : NULL; m_Position = m_Next++; } return NULL; } return *line ? line : NULL; } }; class cPlsReader : public cPlaylistReader { public: cPlsReader(cPlaylist& Playlist) : cPlaylistReader(Playlist), m_Current(0) {} protected: int m_Current; virtual char *Parse(char *line) { char *t = strchr(line, '='); if(t) { int n; if(!strncasecmp(line, "file", 4) && 1 == sscanf(line + 4, "%d=", &n)) { m_Current = n; m_Position = n; if(*(t+1)) return t+1; } else if(!strncasecmp(line, "title", 5) && 1 == sscanf(line + 5, "%d=", &n)) { if(*(t+1)) { if(n == m_Current) Prev()->Title = t; else m_Title = t; } } //else if(!strncasecmp(line, "length", 6) && // 1 == sscanf(line + 4, "%d=", &n)) { //} } return NULL; } }; class cAsxReader : public cPlaylistReader { public: cAsxReader(cPlaylist& Playlist) : cPlaylistReader(Playlist) {} protected: virtual char *Parse(char *line) { char *pt = strstr(line, ""); if(!pt) pt = strstr(line, ""); if(pt) { pt += 7; if(strstr(line, "</")) *strstr(line, "</") = 0; m_Title = pt; } if(*m_Title) { pt = strstr(line, "<ENTRY>"); if(!pt) pt = strstr(line, "<entry>"); if(pt) { if(*m_Title && Prev()) { Prev()->Title = m_Title; m_Title = NULL; } } } return NULL; } }; // // cPlaylist // cPlaylist::cPlaylist() { m_Origin = eImplicit; m_Menu = NULL; m_Scanner = NULL; m_Current = NULL; m_Version = 1; } cPlaylist::~cPlaylist() { if(m_Scanner) { m_Scanner->CancelScanner(); delete m_Scanner; } if(m_Origin == eImplicit) StoreCache(); } void cPlaylist::Listen(cPlaylistChangeNotify *Menu) { cMutexLock ml(&m_Lock); m_Menu = Menu; } void cPlaylist::PlaylistChanged(const cPlaylistItem *Item) { cMutexLock ml(&m_Lock); /*if(m_Origin == eImplicit)*/ Sort(); if(m_Menu) m_Menu->PlaylistChanged(Item); } void cPlaylist::Sort(void) { cMutexLock ml(&m_Lock); cListBase::Sort(); m_Version++; } int cPlaylist::Count(void) const { return cListBase::Count(); } cPlaylistItem *cPlaylist::Next(const cPlaylistItem *i) { cMutexLock ml(&m_Lock); return i ? cList<cPlaylistItem>::Next(i) : cList<cPlaylistItem>::First(); } cPlaylistItem *cPlaylist::Current(void) { cMutexLock ml(&m_Lock); return m_Current ?: First(); } void cPlaylist::Del(cPlaylistItem *it) { cMutexLock ml(&m_Lock); if(!it || Count() < 2) return; if(m_Current == it) m_Current = cList<cPlaylistItem>::Next(Current()) ?: cList<cPlaylistItem>::Prev(Current()); cListBase::Del(it); m_Version++; } void cPlaylist::SetCurrent(cPlaylistItem *current) { cMutexLock ml(&m_Lock); m_Current = current; } cPlaylistItem *cPlaylist::Next(void) { cMutexLock ml(&m_Lock); if(Current()) return m_Current = (cList<cPlaylistItem>::Next(Current()) ?: First()); return NULL; } cPlaylistItem *cPlaylist::Prev(void) { cMutexLock ml(&m_Lock); if(Current()) return m_Current = (cList<cPlaylistItem>::Prev(Current()) ?: Last()); return NULL; } bool cPlaylist::StoreCache(void) { if(!xc.cache_implicit_playlists || m_Origin != eImplicit || !*m_Folder) return false; cString Name = cString::sprintf("%s%s", *m_Folder, PLAYLIST_CACHE); int len = strlen(m_Folder), entries = 0; FILE *f = NULL; for(cPlaylistItem *i = First(); i; i=Next(i)) { // store only items in "current" root folder if(!strncmp(i->Filename, m_Folder, len)) { if(/**i->Title ||*/ *i->Artist || *i->Album) { cString Filename = ((*i->Filename) + len); // relative if(entries < 1) { f = fopen(Name, "w"); if(!f) { LOGERR("creation of metadata cache %s%s failed", *m_Folder, PLAYLIST_CACHE); return false; } fprintf(f, "[playlist]\r\n"); } entries++; fprintf(f, "File%d=%s\r\n", entries, *Filename); if(*i->Title && (*i->Title)[0]) fprintf(f, "Title%d=%s\r\n", entries, *i->Title); if(*i->Tracknumber && (*i->Tracknumber)[0]) fprintf(f, "Tracknumber%d=%s\r\n", entries, *i->Tracknumber); if(*i->Artist && (*i->Artist)[0]) fprintf(f, "Artist%d=%s\r\n", entries, *i->Artist); if(*i->Album && (*i->Album)[0]) fprintf(f, "Album%d=%s\r\n", entries, *i->Album); } } } if(entries > 0) { fprintf(f, "NumberOfEntries=%d\r\nVersion=2\r\n", entries); fclose(f); return true; } return false; } static const char *strchrnext(const char *s, char c) { return (s = strchr(s, c)) ? ((*(s+1))?(s+1):NULL) : NULL; } bool cPlaylist::ReadCache(void) { if(xc.cache_implicit_playlists && m_Origin == eImplicit && *m_Folder) { cString Name = cString::sprintf("%s%s", *m_Folder, PLAYLIST_CACHE); FILE *f = fopen(Name, "r"); if(f) { int len = strlen(m_Folder); cPlaylistItem *it = NULL; cReadLine r; const char *pt; while(NULL != (pt = r.Read(f))) { if(!strncmp(pt, "File", 4)) { it = NULL; const char *Filename = strchrnext(pt+4, '='); if(Filename && *Filename) { for(cPlaylistItem *i = First(); i; i=Next(i)) { if(!strncmp(i->Filename, m_Folder, len)) { if(!strcmp(*i->Filename + len, Filename)) { it = i; break; } } } } } else if(it && !strncmp(pt, "Title", 5)) { it->Title = strchrnext(pt, '='); } else if(it && !strncmp(pt, "Tracknumber", 11)) { it->Tracknumber = strchrnext(pt, '='); } else if(it && !strncmp(pt, "Artist", 6)) { it->Artist = strchrnext(pt, '='); } else if(it && !strncmp(pt, "Album", 5)) { it->Album = strchrnext(pt, '='); } else { /*it = NULL;*/ } } fclose(f); return true; } } return false; } #if 0 static FILE *open_http(const char *PlaylistFile) { char file[1024] = "", host[128] = "", pt; int fd, port = 80; strn0cpy(host, PlaylistFile+strlen("http://"), sizeof(host)-1); pt = strchr(host, '/'); if(pt) { strn0cpy(file, pt, sizeof(file)-1); *pt = 0; } pt = strchr(host, ':'); if(pt) { *pt++ = 0; port = atoi(pt); } fd = tcp_connect(host, port); if(fd < 0) { LOGERR("TCP connect failed"); return NULL; } int len = asprintf(&pt, "GET %s HTTP/1.1" "\r\n" "Host: %s" "\r\n" "\r\n", file, host); if(len != write(fd, pt, len)) { LOGERR("HTTP request write failed"); free(pt); close(fd); return NULL; } free(pt); int state = 0; FILE *f = fdopen(fd, "r"); cReadLine r; while(state >= 0 && NULL != (pt = r.Read(f))) { switch(state) { case 0: if(!strncmp(pt, "HTTP/1", 6) || !strstr(pt, " 200 ")) { LOGERR("HTTP error: %s", pt); fclose(f); return NULL; } state = 1; break; case 1: if(strcmp(pt, "\r\n")) break; return f; default: break; } } fclose(f); return NULL; } #endif int cPlaylist::ScanFolder(const char *FolderName, bool Recursive, bool (*Filter)(const char *)) { cMutexLock ml(&m_Lock); static int depth = 0; DIR *d = opendir(FolderName); if (d) { LOGDBG("ScanFolder(%s)", FolderName); struct dirent *e; int n = 0, warn = -1; while ((e = readdir(d)) != NULL) { cString Buffer = cString::sprintf("%s%s", FolderName, e->d_name); struct stat st; if (stat(Buffer, &st) == 0) { if(S_ISDIR(st.st_mode)) { if (Recursive && !S_ISLNK(st.st_mode)) { /* don't want to loop ... */ if(depth > 4) { LOGMSG("ScanFolder: Too deep directory tree"); } else if(e->d_name[0]=='.') { } else { if(n<MAX_PLAYLIST_FILES) { depth++; /* limit depth */ Buffer = cString::sprintf("%s/", *Buffer); n += ScanFolder(Buffer, Recursive, Filter); depth--; } else { if(!++warn) LOGMSG("ScanFolder: Found over %d matching files, list truncated!", n); break; } } } } else /* == if(!S_ISDIR(st.st_mode))*/ { // check symlink destination if (S_ISLNK(st.st_mode)) { Buffer = ReadLink(Buffer); if (!*Buffer) continue; if (stat(Buffer, &st) != 0) continue; } if((*Filter)(Buffer)) { /* TODO: Should ScanDir add contents of playlist files ... ? */ if(Filter == &config_t::IsPlaylistFile || !xc.IsPlaylistFile(Buffer)) { n++; if(n<MAX_PLAYLIST_FILES) { Add(new cPlaylistItem(e->d_name, FolderName)); //LOGDBG("ScanFolder: %s", e->d_name); } else { if(!++warn) LOGMSG("ScanFolder: Found over %d matching files, list truncated!", n); break; } } } } } } LOGDBG("ScanFolder: Found %d matching files from %s", n, FolderName); closedir(d); return n; } LOGERR("ScanFolder: Error opening %s", FolderName); return 0; } void cPlaylist::StartScanner(void) { cMutexLock ml(&m_Lock); if(m_Scanner) { if(m_Scanner->Active()) return; delete m_Scanner; m_Scanner = NULL; } /* check if cache is already up-to-date */ cString CacheName = cString::sprintf("%s%s", *m_Folder, PLAYLIST_CACHE); struct stat stf, stc; if(!stat(m_Folder, &stf)) { if(!stat(CacheName, &stc)) { //LOGDBG("ID3 Cache modified %d, folder modified %d, diff %d", // (unsigned int)stc.st_mtime, (unsigned int)stf.st_mtime, // (unsigned int)(stc.st_mtime - stf.st_mtime)); if(stc.st_mtime >= stf.st_mtime) { if(ReadCache()) { LOGDBG("cPlaylist: using up-to-date ID3 cache"); //LOGMSG(" Cache read OK."); return; } LOGMSG("cPlaylist: ID3 cache read FAILED"); } else { LOGDBG("cPlaylist: ID3 cache not up-to-date, using old cache and scanning for changes"); ReadCache(); } } //else LOGERR("cPlaylist: stat(%s) failed"); } //else LOGERR("cPlaylist: stat(%s) failed"); if(xc.enable_id3_scanner) { m_Scanner = new cID3Scanner(*this); m_Scanner->Start(); } } int cPlaylist::ReadPlaylist(const char *file) { static int depth = 0; /* limit recursion */ cPipe p; cPlaylistReader *parser = NULL; FILE *f; if(strncmp(file, "http:", 5) && strncmp(file, "https:", 6)) { f = fopen(file, "r"); } else { // fetch playlist from server using curl LOGDBG("cPlaylist: fetching remote playlist from %s", file); cString Cmd = cString::sprintf("curl %s", file); if(!p.Open(Cmd, "r")) { LOGERR("cPlaylist: CURL command (%s) failed", *Cmd); return false; } // process as normal file f = p; } if(f) { LOGDBG("cPlaylist: parsing %s", file); const char *ext = strrchr(file, '.'); if(!strcasecmp(ext, ".pls")) parser = new cPlsReader(*this); else if(!strcasecmp(ext, ".asx")) parser = new cAsxReader(*this); else /*if(!strcasecmp(ext, ".m3u"))*/ parser = new cM3uReader(*this); /* parses plain lists (.ram, ...) too ...*/ /* get folder */ cString Folder = file; const char *folder = strrchr(Folder, '/'); if (folder) Folder.Truncate(folder - Folder + 1); int n = 0; cReadLine r; char *pt; while(NULL != (pt = r.Read(f)) && n < MAX_PLAYLIST_FILES) { if(NULL != (pt = parser->Parse(pt))) { if(xc.IsPlaylistFile(pt)) { parser->ResetCache(); LOGDBG("cPlaylist: found playlist inside playlist"); if(depth > 4) LOGMSG("cPlaylist: recursion too deep, skipped %s", pt); else { depth++; if(*pt == '/' || (strstr(pt,"://")+1 == strchr(pt,'/') && strchr(pt,'/') - pt < 8)) n += ReadPlaylist(pt); else n += ReadPlaylist(cString::sprintf("%s%s", *Folder, pt)); depth--; } } else { if(*pt == '/' || (strstr(pt,"://")+1 == strchr(pt,'/') && strchr(pt,'/') - pt < 8)) { // absolute path Add(new cPlaylistItem(pt)); if(parser->Title()) Last()->Title = parser->Title(); } else { // relative path Add(new cPlaylistItem(pt, Folder, parser->Title())); } Last()->Position = parser->Position(); parser->ResetCache(); //LOGDBG("read_playlist: %s", pt); n++; } } } if(! (FILE*) p) fclose(f); if(n >= MAX_PLAYLIST_FILES) LOGMSG("cPlaylist: Found over %d matching files, list truncated!", n); LOGDBG("cPlaylist: Found %d matching files", n); return n; } LOGERR("cPlaylist: Error opening %s", file); return 0; } static cString LastDir(cString& path) { cString tmp = path; const char *pt = strrchr(tmp, '/'); if(pt && pt > *tmp) { tmp.Truncate(pt - tmp); pt = strrchr(tmp, '/'); if(pt) return cString(pt+1); } return cString(NULL); } bool cPlaylist::Read(const char *PlaylistFile, bool Recursive) { cMutexLock ml(&m_Lock); bool Result = true; // extract playlist root folder if(!*m_Folder) { const char *pt; m_Folder = PlaylistFile; if (NULL != (pt=strrchr(m_Folder, '/'))) m_Folder.Truncate(pt - m_Folder + 1); } if(xc.IsPlaylistFile(PlaylistFile)) { // Read playlist file Result = ReadPlaylist(PlaylistFile); m_Origin = ePlaylist; cString dir = LastDir(m_Folder); const char *name = strrchr(PlaylistFile, '/'); name = name ? name+1 : NULL; if(*dir && name) m_Name = cString::sprintf("%s - %s", *dir, name); else m_Name = name ?: ""; strip_extension(m_Name); } else if(PlaylistFile[ 0] == '/' && PlaylistFile[strlen(PlaylistFile)-1] == '/') { // Scan folder Result = ScanFolder(PlaylistFile, Recursive) > 0; m_Origin = eImplicit; Sort(); if(!*m_Name) { m_Name = PlaylistFile; m_Name.Truncate( strrchr(m_Name, '/') - m_Name); if(strrchr(m_Name, '/')) { cString dir = LastDir(m_Name); if(*dir) m_Name = cString::sprintf("%s - %s", *dir, strrchr(m_Name, '/')+1); else m_Name = strrchr(m_Name, '/')+1; } } } else { // Single file Add(new cPlaylistItem(PlaylistFile)); m_Origin = eImplicit; if(!*m_Name) { m_Name = LastDir(m_Folder); if(!*m_Name) m_Name = ""; } } if(Count() < 1) { LOGMSG("Empty playlist %s !", PlaylistFile); Add(new cPlaylistItem(PlaylistFile)); } m_Version++; return Result; } cString cPlaylist::EscapeMrl(const char *mrl) { static const uint8_t hex[16] = {'0','1','2','3','4','5','6','7','8','9','a','b','c','d','e','f'}; const uint8_t *fn = (const uint8_t*)mrl; int size = strlen(mrl) + 16; char *buf = (char *)malloc(size); int i = 0, found = 0; LOGDBG("cPlaylist::EscapeMrl('%s')", fn); // Wait for first '/' (do not escape mrl start dvd:/, http://a@b/, ...) if (*fn == '/') found = 3; while (*fn) { if(size-7 < i) buf = (char *)realloc(buf, (size=size+16)); switch (*fn) { case 1 ... ' ': case 127 ... 255: case '#': case '%': case ':': case ';': case '\'': case '\"': case '(': case ')': if (found > 2) { buf[i++] = '%'; buf[i++] = hex[(*fn & 0xf0)>>4]; buf[i++] = hex[(*fn & 0x0f)]; break; } default: // file:/... -> only one '/' before escaping // http://.../ --> three '/' before escaping if(!found && (fn[0] == ':' && fn[1] == '/')) { if(fn[2] == '/') { // ex. http://user:pass@host/... --> wait for third '/' buf[i++] = *fn++; buf[i++] = *fn++; found += 2; } else { // ex. file:/local_file buf[i++] = *fn++; found += 3; } } else if(*fn == '/') { found++; } buf[i++] = *fn; break; } fn++; } buf[i] = 0; LOGDBG(" --> '%s'", buf); return cString(buf, true); } cString cPlaylist::GetEntry(cPlaylistItem *i, bool isPlaylist, bool isCurrent) { cString Entry = ""; if ((*i->Artist && xc.playlist_artist) || (*i->Album && xc.playlist_album)) { Entry = cString::sprintf("%s%s%s%s%s%s(%s%s%s)", isPlaylist ? (isCurrent ? "*" : " ") : "", isPlaylist ? "\t" : " ", xc.playlist_tracknumber ? (*i->Tracknumber ?: "") : "", xc.playlist_tracknumber ? (*i->Tracknumber ? " - " : "") : "", *i->Title, isPlaylist ? "\t" : " ", xc.playlist_artist ? (*i->Artist ?: "") : "", xc.playlist_artist && xc.playlist_album ? (*i->Artist && *i->Album ? ":" : "") : "", xc.playlist_album ? (*i->Album ?: "") : ""); } else { Entry = cString::sprintf("%s%s%s%s%s", isPlaylist ? (isCurrent ? "*" : " ") : "", isPlaylist ? "\t" : " ", xc.playlist_tracknumber ? (*i->Tracknumber ?: "") : "", xc.playlist_tracknumber ? (*i->Tracknumber ? " - " : "") : "", *i->Title); } return Entry; }