From c4aced1f3189bc945c2ff07e99943ac2c7dfb39c Mon Sep 17 00:00:00 2001 From: Jon Bergli Heier Date: Wed, 21 May 2014 23:21:59 +0200 Subject: [PATCH 1/8] Added working support for cuesheets. This change adds support for cuesheets by passing a track parameter around which indicates which track in the cuesheet should be played. The actual audio file is split with ffmpeg using the -ss and -t parameters, which are filled by the decoder when the file extension is '.cue'. cuesheet.py is added to parse cuesheets. This is taken from a separate project so it's a bit messy and contains some stuff that isn't used. Metadata is also fetched from the cuesheet per track. --- audiotranscode/__init__.py | 49 ++++++++-- cherrymusicserver/cherrymodel.py | 19 +++- cherrymusicserver/cuesheet.py | 130 +++++++++++++++++++++++++++ cherrymusicserver/httphandler.py | 10 ++- cherrymusicserver/metainfo.py | 31 ++++++- res/dist/cherrymusic.dist.js | 23 ++--- res/js/cherrymusic.js | 4 +- res/js/mediabrowser.js | 7 +- res/js/playlistmanager.js | 10 ++- res/templates/mediabrowser-file.html | 4 +- 10 files changed, 252 insertions(+), 35 deletions(-) create mode 100644 cherrymusicserver/cuesheet.py diff --git a/audiotranscode/__init__.py b/audiotranscode/__init__.py index 6962703b..be41bbf5 100644 --- a/audiotranscode/__init__.py +++ b/audiotranscode/__init__.py @@ -12,6 +12,7 @@ 'm4a' : 'audio/m4a', 'wav' : 'audio/wav', 'wma' : 'audio/x-ms-wma', + 'cue': '', # FIXME: What should we put here? .cue doesn't actually contain any audio. } class Transcoder(object): @@ -52,13 +53,44 @@ def __init__(self, filetype, command): self.filetype = filetype self.mimetype = MimeTypes[filetype] - def decode(self, filepath, starttime=0): + def decode(self, filepath, starttime=0, track=None): cmd = self.command[:] + ext = os.path.splitext(filepath)[1] + if ext == '.cue': + if not track: + track = 1 + else: + track = int(track) + from cherrymusicserver.cuesheet import Cuesheet + cue = Cuesheet(filepath) + audio_filepath = None + for cdtext in cue.info[0].cdtext: + if cdtext.type == 'FILE': + # Set the actual filepath from the FILE field + audio_filepath = os.path.join(os.path.dirname(filepath), cdtext.value[0]) + break + if audio_filepath is None or not os.path.exists(audio_filepath): + raise DecodeError('Audiofile not found for cuesheet %s'%filepath) + filepath = audio_filepath + track_n = track + track = cue.tracks[track-1] + if 'STARTTIME' in cmd: + cmd[cmd.index('STARTTIME')] = str(track.get_start_time()) + if 'LENGTH' in cmd: + nexttrack = cue.get_next(track) + if nexttrack: + track.nextstart = nexttrack.get_start_time() + cmd[cmd.index('LENGTH')] = str(track.get_length()) + else: + # Last track doesn't have a next track + # XXX: Actually we should remove the -t option altogether here + cmd[cmd.index('LENGTH')] = '99999' + else: + if 'STARTTIME' in cmd: + hours, minutes, seconds = starttime//3600, starttime//60%60, starttime%60 + cmd[cmd.index('STARTTIME')] = '%d:%d:%d' % (hours, minutes, seconds) if 'INPUT' in cmd: cmd[cmd.index('INPUT')] = filepath - if 'STARTTIME' in cmd: - hours, minutes, seconds = starttime//3600, starttime//60%60, starttime%60 - cmd[cmd.index('STARTTIME')] = '%d:%d:%d' % (hours, minutes, seconds) return subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=Transcoder.devnull @@ -107,6 +139,7 @@ class AudioTranscode: Decoder('aac' , ['faad', '-w', 'INPUT']), Decoder('m4a' , ['faad', '-w', 'INPUT']), Decoder('wav' , ['cat', 'INPUT']), + Decoder('cue' , ['ffmpeg', '-ss', 'STARTTIME', '-t', 'LENGTH', '-i', 'INPUT', '-f', 'wav', '-']), ] def __init__(self,debug=False): @@ -125,7 +158,7 @@ def _filetype(self, filepath): if '.' in filepath: return filepath.lower()[filepath.rindex('.')+1:] - def _decode(self, filepath, decoder=None, starttime=0): + def _decode(self, filepath, decoder=None, starttime=0, track=None): if not os.path.exists(filepath): filepath = os.path.abspath(filepath) raise DecodeError('File not Found! Cannot decode "file" %s'%filepath) @@ -139,7 +172,7 @@ def _decode(self, filepath, decoder=None, starttime=0): break if self.debug: print(decoder) - return decoder.decode(filepath, starttime=starttime) + return decoder.decode(filepath, starttime=starttime, track=track) def _encode(self, audio_format, decoder_process, bitrate=None,encoder=None): if not audio_format in self.availableEncoderFormats(): @@ -166,11 +199,11 @@ def transcode(self, in_file, out_file, bitrate=None): fh.close() def transcodeStream(self, filepath, newformat, bitrate=None, - encoder=None, decoder=None, starttime=0): + encoder=None, decoder=None, starttime=0, track=None): decoder_process = None encoder_process = None try: - decoder_process = self._decode(filepath, decoder, starttime=starttime) + decoder_process = self._decode(filepath, decoder, starttime=starttime, track=track) encoder_process = self._encode(newformat, decoder_process,bitrate=bitrate,encoder=encoder) while encoder_process.poll() == None: data = encoder_process.stdout.read(AudioTranscode.READ_BUFFER) diff --git a/cherrymusicserver/cherrymodel.py b/cherrymusicserver/cherrymodel.py index a83a5a25..2af477da 100644 --- a/cherrymusicserver/cherrymodel.py +++ b/cherrymusicserver/cherrymodel.py @@ -163,10 +163,19 @@ def listdir(self, dirpath, filterstr=''): musicentry.count_subfolders_and_files() return musicentries + @classmethod + def addCueSheet(cls, filepath, list): + from cherrymusicserver.cuesheet import Cuesheet + cue = Cuesheet(filepath) + for track in range(len(cue.tracks)): + list.append(MusicEntry(strippath(filepath), track = track+1)) + @classmethod def addMusicEntry(cls, fullpath, list): if os.path.isfile(fullpath): - if CherryModel.isplayable(fullpath): + if iscuesheet(fullpath): + CherryModel.addCueSheet(fullpath, list) + elif CherryModel.isplayable(fullpath): list.append(MusicEntry(strippath(fullpath))) else: list.append(MusicEntry(strippath(fullpath), dir=True)) @@ -313,6 +322,9 @@ def isplayable(cls, filename): is_empty_file = os.path.getsize(CherryModel.abspath(filename)) == 0 return is_supported_ext and not is_empty_file +def iscuesheet(filename): + ext = os.path.splitext(filename)[1] + return ext.lower() == '.cue' def strippath(path): if path.startswith(cherry.config['media.basedir']): @@ -325,7 +337,7 @@ class MusicEntry: # check if there are playable meadia files or other folders inside MAX_SUB_FILES_ITER_COUNT = 100 - def __init__(self, path, compact=False, dir=False, repr=None, subdircount=0, subfilescount=0): + def __init__(self, path, compact=False, dir=False, repr=None, subdircount=0, subfilescount=0, track=None): self.path = path self.compact = compact self.dir = dir @@ -336,6 +348,8 @@ def __init__(self, path, compact=False, dir=False, repr=None, subdircount=0, sub self.subfilescount = subfilescount # True when the exact amount of files is too big and is estimated self.subfilesestimate = False + # Track number for multi-track files + self.track = track def count_subfolders_and_files(self): if self.dir: @@ -381,6 +395,7 @@ def to_dict(self): return {'type': 'file', 'urlpath': urlpath, 'path': self.path, + 'track': self.track, 'label': simplename} def __repr__(self): diff --git a/cherrymusicserver/cuesheet.py b/cherrymusicserver/cuesheet.py new file mode 100644 index 00000000..5fd757b9 --- /dev/null +++ b/cherrymusicserver/cuesheet.py @@ -0,0 +1,130 @@ +# Python cuesheet parsing +# Copyright (C) 2009-2014 Jon Bergli Heier + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see +# + +import re + +cdtext_re = { + 'REM': r'^(REM) (.+)$', + 'PERFORMER': r'^(PERFORMER) "?(.+?)"?$', + 'TITLE': r'^(TITLE) "?(.+?)"?$', + #'FILE': r'^(FILE) "?(.+?)"? (BINARY|MOTOROLA|AIFF|WAVE|MP3)$', + # XXX: The above line is the only correct one according to the spec, but some people + # seem to think that putting stuff like FLAC here instead is a good idea. + 'FILE': r'^(FILE) "?(.+?)"? (\w+)$', + 'TRACK': r'^(TRACK) (\d+) (AUDIO|CDG|MODE1/2048|MODE1/2352|MODE2/2336|MODE2/2352|CDI/2336|CDI2352)$', + 'INDEX': r'^(INDEX) (\d+) (\d+):(\d+):(\d+)$', + 'FLAGS': r'^((?:DCP|4CH|PRE|SCMS) ?){1,4}$', + 'ISRC': r'^(ISRC) (\w{5}\d{7})$', + 'SONGWRITER': r'^(SONGWRITER) "?(.+?)"?$', + 'CATALOG': r'^(CATALOG) (\d{13})$', +} + +for k, v in cdtext_re.items(): + cdtext_re[k] = re.compile(v) + +class CDText(object): + def __init__(self, str): + name = str.split()[0] + self.re = cdtext_re[name] + l = self.parse(str) + self.type, self.value = l[0], l[1:] + if type(self.value) == tuple and len(self.value) == 1: + self.value = self.value[0] + + def __repr__(self): + return '' % (self.type, self.value) + + def __str__(self): + return repr(self) + + def parse(self, str): + r = self.re.match(str) + if not r: + return None, None + return r.groups() + +class FieldDescriptor(object): + def __init__(self, field): + self.field = field + + def __get__(self, instance, owner): + def find(name): + for l in instance.cdtext: + if l.type == name: + return l + cdtext = find(self.field) + return cdtext.value if cdtext else None + +class Track(object): + def __init__(self): + self.cdtext = [] + self.abs_tot, self.abs_end, self.nextstart = 0, 0, None + + def add(self, cdtext): + self.cdtext.append(cdtext) + + def set_abs_tot(self, tot): + self.abs_tot = tot + + def set_abs_end(self, end): + self.abs_end + + def get_start_time(self): + index = self.index + return int(index[1])*60 + int(index[2]) + float(index[3])/75 + + def get_length(self): + return self.nextstart - self.get_start_time() + +for f in cdtext_re.keys(): + setattr(Track, f.lower(), FieldDescriptor(f)) + +class Cuesheet(object): + def __init__(self, filename = None, fileobj = None): + if not fileobj and filename: + fileobj = open(filename, 'rb') + if fileobj: + self.parse(fileobj) + + def parse(self, f): + info = [] + tracks = [] + track = Track() + info.append(track) + if not f.read(3) == b'\xef\xbb\xbf': + f.seek(0) + for line in f: + line = line.strip() + line = line.decode('utf-8') + if not len(line): + continue + cdtext = CDText(line) + if cdtext.type == 'TRACK': + track = Track() + tracks.append(track) + track.add(cdtext) + self.info = info + self.tracks = tracks + + def get_next(self, track): + found = False + for i in self.tracks: + if found: + return i + elif i == track: + found = True + return None diff --git a/cherrymusicserver/httphandler.py b/cherrymusicserver/httphandler.py index 1dc43c1a..35f30025 100644 --- a/cherrymusicserver/httphandler.py +++ b/cherrymusicserver/httphandler.py @@ -253,13 +253,17 @@ def trans(self, newformat, *path, **params): fullpath = os.path.join(cherry.config['media.basedir'], path) starttime = int(params.pop('starttime', 0)) + if 'track' in params: + track = params.pop('track') + else: + track = None transcoder = audiotranscode.AudioTranscode() mimetype = transcoder.mimeType(newformat) cherrypy.response.headers["Content-Type"] = mimetype try: return transcoder.transcodeStream(fullpath, newformat, - bitrate=bitrate, starttime=starttime) + bitrate=bitrate, starttime=starttime, track=track) except audiotranscode.TranscodeError as e: raise cherrypy.HTTPError(404, e.value) trans.exposed = True @@ -652,10 +656,10 @@ def export_playlists(self, format, all=False, hostaddr=''): return zip.getbytes() export_playlists.exposed = True - def api_getsonginfo(self, path): + def api_getsonginfo(self, path, track = None): basedir = cherry.config['media.basedir'] abspath = os.path.join(basedir, path) - return json.dumps(metainfo.getSongInfo(abspath).dict()) + return json.dumps(metainfo.getSongInfo(abspath, track).dict()) def api_getencoders(self): return json.dumps(audiotranscode.getEncoders()) diff --git a/cherrymusicserver/metainfo.py b/cherrymusicserver/metainfo.py index 238152fe..515a29ba 100644 --- a/cherrymusicserver/metainfo.py +++ b/cherrymusicserver/metainfo.py @@ -30,7 +30,7 @@ # from cherrymusicserver import log -import sys +import sys, os from tinytag import TinyTag @@ -50,7 +50,34 @@ def dict(self): 'length': self.length } -def getSongInfo(filepath): +def getSongInfo(filepath, _track=None): + ext = os.path.splitext(filepath)[1] + if ext == '.cue' and _track: + from cherrymusicserver.cuesheet import Cuesheet + cue = Cuesheet(filepath) + info = cue.info[0] + artist = info.performer or '-' + album = info.title or '-' + title = '-' + _track = int(_track) + track = cue.tracks[_track-1] + artist = track.performer or artist + title = track.title or title + if _track < len(cue.tracks): + track.nextstart = cue.get_next(track).get_start_time() + audiolength = track.get_length() + else: + # FIXME + try: + #with audioread.audio_open(info.file[0]) as f: + # lasttrack.nextstart = f.duration + audiofilepath = os.path.join(os.path.dirname(filepath), info.file[0]) + tag = TinyTag.get(audiofilepath) + except Exception: + audiolength = 0 + else: + audiolength = tag.duration + return Metainfo(artist, album, title, _track, audiolength) try: tag = TinyTag.get(filepath) except LookupError: diff --git a/res/dist/cherrymusic.dist.js b/res/dist/cherrymusic.dist.js index 43ef289b..67727be9 100644 --- a/res/dist/cherrymusic.dist.js +++ b/res/dist/cherrymusic.dist.js @@ -972,7 +972,7 @@ return this} $(document).on('click.bs.alert.data-api',dismiss,Alert.prototype.close)}(window.jQuery);;var ManagedPlaylist=function(playlistManager,playlist,options){this.playlistManager=playlistManager;this.id=options.id;this.name=options.name;this.closable=options.closable;this.public=options.public;this.owner=options.owner;this.saved=options.saved;this.reason_open=options.reason_open;this.jplayerplaylist;this._init(playlist,playlistManager)} ManagedPlaylist.prototype={_init:function(playlist,playlistManager){var self=this;this.playlistSelector=self._createNewPlaylistContainer();for(var i=0;iul.playlist-container-list").sortable({axis:"y",delay:150,helper:'clone',update:function(e,ui){self.jplayerplaylist.scan();}});$(self.playlistSelector+">ul.playlist-container-list").disableSelection();$(this.playlistSelector).bind('requestPlay',function(event,playlistSelector){self.playlistManager.setPlayingPlaylist(self.playlistManager.htmlid2plid(playlistSelector));});$(this.playlistSelector).bind('removedItem',function(event,playlistSelector){self.setSaved(false);});$(this.playlistSelector).bind('sortedItems',function(event,playlistSelector){self.setSaved(false);});$(this.playlistSelector).bind('addedItem',function(event,playlistSelector){self.setSaved(false);});},setSaved:function(issaved){this.saved=issaved;this.playlistManager.refreshCommands();this.playlistManager.refreshTabs();},wasSaved:function(){return this.saved;},_createNewPlaylistContainer:function(){var playlistContainerParent=this.playlistManager.cssSelectorPlaylistContainerParent;var id=this.playlistManager.plid2htmlid(this.id);$(playlistContainerParent).append('');return'#'+id;},getCanonicalPlaylist:function(){var canonical=[];for(var i=0;iul.playlist-container-list").sortable({axis:"y",delay:150,helper:'clone',update:function(e,ui){self.jplayerplaylist.scan();}});$(self.playlistSelector+">ul.playlist-container-list").disableSelection();$(this.playlistSelector).bind('requestPlay',function(event,playlistSelector){self.playlistManager.setPlayingPlaylist(self.playlistManager.htmlid2plid(playlistSelector));});$(this.playlistSelector).bind('removedItem',function(event,playlistSelector){self.setSaved(false);});$(this.playlistSelector).bind('sortedItems',function(event,playlistSelector){self.setSaved(false);});$(this.playlistSelector).bind('addedItem',function(event,playlistSelector){self.setSaved(false);});},setSaved:function(issaved){this.saved=issaved;this.playlistManager.refreshCommands();this.playlistManager.refreshTabs();},wasSaved:function(){return this.saved;},_createNewPlaylistContainer:function(){var playlistContainerParent=this.playlistManager.cssSelectorPlaylistContainerParent;var id=this.playlistManager.plid2htmlid(this.id);$(playlistContainerParent).append('');return'#'+id;},getCanonicalPlaylist:function(){var canonical=[];for(var i=0;iwasplayermost){wasplayermost=this.jplayerplaylist.playlist[i].wasPlayed;}} @@ -1007,12 +1007,13 @@ var otherId=this.managedPlaylists[i '+availablejPlayerFormats[i]+' @ '+transurl);}}} +return;}else{for(var i=0;i '+availablejPlayerFormats[i]+' @ '+transurl);}}} if(formats.length==0){window.console.log('no suitable encoder available! Try installing vorbis-tools or lame!');return;}} -return track;},addSong:function(path,title,plid,animate){"use strict";var self=this;if(typeof animate==='undefined'){animate=true;} -var track={title:title,url:path,wasPlayed:0,} +return track;},addSong:function(path,title,track_,plid,animate){"use strict";var self=this;if(typeof animate==='undefined'){animate=true;} +var track={title:title,url:path,track:track_,wasPlayed:0,} var playlist;if(plid){playlist=this.getPlaylistById(plid);} if(typeof playlist=='undefined'){playlist=this.getEditingPlaylist();} playlist.addTrack(track,animate);if(!jPlayerIsPlaying()&&playlist.jplayerplaylist.playlist.length==1){if(userOptions.misc.autoplay_on_add){playlist.makeThisPlayingPlaylist();playlist.jplayerplaylist.play(0);}else{playlist.jplayerplaylist.select(0);}} @@ -1020,7 +1021,7 @@ var success=function(data){var metainfo=$.parseJSON(data);var any_info_received= if(metainfo.title.length>0&&metainfo.artist.length>0){track.title=metainfo.artist+' - '+metainfo.title;if(metainfo.track.length>0){track.title=metainfo.track+' '+track.title;if(metainfo.track.length<2){track.title='0'+track.title;}} any_info_received=true;} if(any_info_received){self.getEditingPlaylist().jplayerplaylist._refresh(true);}} -window.setTimeout(function(){api('getsonginfo',{'path':decodeURIComponent(path)},success,errorFunc('error getting song metainfo'),true);},1000);},clearPlaylist:function(){"use strict";this.getEditingPlaylist().remove();if(this.getEditingPlaylist()==this.getPlayingPlaylist()){$(this.cssSelectorjPlayer).jPlayer("clearMedia");} +window.setTimeout(function(){api('getsonginfo',{'path':decodeURIComponent(path),'track':track_},success,errorFunc('error getting song metainfo'),true);},1000);},clearPlaylist:function(){"use strict";this.getEditingPlaylist().remove();if(this.getEditingPlaylist()==this.getPlayingPlaylist()){$(this.cssSelectorjPlayer).jPlayer("clearMedia");} return false;},displayCurrentSong:function(){var pl=this.getPlayingPlaylist();if(typeof pl==='undefined'){return;} var jPlaylist=pl.jplayerplaylist;var songtitle='';var tabtitle='CherryMusic';if(typeof this.jPlayerInstance!=='undefined'){var currentTitle=this.jPlayerInstance.data().jPlayer.status.media.title;if(typeof currentTitle!=='undefined'){songtitle=currentTitle;tabtitle=currentTitle+' | CherryMusic';}} $('.cm-songtitle').html(songtitle);$('title').text(tabtitle);},rememberPlaylist:function(){"use strict";var self=this;var canonicalPlaylists=[] @@ -1058,12 +1059,12 @@ this.render();$(cssSelector).off('click');$(cssSelector).on('click','.list-dir', $('#changeAlbumArt').modal('show');});$(cssSelector).on('click','.mb-view-list-enable',view_list_enable);$(cssSelector).on('click','.mb-view-cover-enable',view_cover_enable);$(cssSelector).on('click','.addAllToPlaylist',function(){if(isplaylist){var pl=playlistManager.newPlaylist([],playlistlabel);}else{var pl=playlistManager.getEditingPlaylist();} MediaBrowser.static._addAllToPlaylist($(this),pl.id);if(isplaylist){pl.setSaved(true);} $(this).blur();return false;});} -MediaBrowser.static={_renderList:function(l,listview){"use strict";var self=this;var html="";$.each(l,function(i,e){switch(e.type){case'dir':html+=MediaBrowser.static._renderDirectory(e,listview);break;case'file':html+=MediaBrowser.static._renderFile(e);break;case'compact':html+=MediaBrowser.static._renderCompactDirectory(e);break;case'playlist':html+=MediaBrowser.static._renderPlaylist(e);break;default:window.console.log('cannot render unknown type '+e.type);}});return html;},_renderMessage:function(msg){var template=templateLoader.cached('mediabrowser-message');return Mustache.render(template,{message:msg});},_renderFile:function(json){var template=templateLoader.cached('mediabrowser-file');var template_data={fileurl:json.urlpath,fullpath:json.path,label:json.label,};return Mustache.render(template,template_data);},_renderDirectory:function(json,listview){var template=templateLoader.cached('mediabrowser-directory');var template_data={isrootdir:json.path&&!json.path.indexOf('/')>0,dirpath:json.path,label:json.label,listview:listview,maychangecoverart:!!isAdmin,coverarturl:encodeURIComponent(JSON.stringify({'directory':json.path})),directoryname:encodeURIComponent(json.path),foldercount:json.foldercount,showfoldercount:json.foldercount>0,filescount:json.filescount,showfilescount:json.filescount>0,filescountestimate:json.filescountestimate,};return Mustache.render(template,template_data);},_renderPlaylist:function(e){var template=templateLoader.cached('mediabrowser-playlist');var template_data={playlistid:e['plid'],isowner:e.owner,candelete:e.owner||isAdmin,playlistlabel:e['title'],username:e['username'],age:time2text(e['age']),username_color:userNameToColor(e.username),publicchecked:e['public']?'checked="checked"':'',publiclabelclass:e['public']?'label-success':'label-default',};return Mustache.render(template,template_data);},_renderCompactDirectory:function(json){var template=templateLoader.cached('mediabrowser-compact');var template_data={filepath:json.urlpath,filter:json.label,filterUPPER:json.label.toUpperCase(),};return Mustache.render(template,template_data);},addThisTrackToPlaylist:function(){"use strict" -playlistManager.addSong($(this).attr("path"),$(this).attr("title"));$(this).blur();return false;},_addAllToPlaylist:function($source,plid){"use strict";$source.find('li .musicfile').each(function(){playlistManager.addSong($(this).attr("path"),$(this).attr("title"),plid);});},albumArtLoader:function(cssSelector){"use strict";var winheight=$(window).height();var scrolled_down=$(cssSelector).scrollTop();var preload_threshold=50;$(cssSelector).find('.list-dir-albumart.unloaded').each(function(idx){var img_pos=$(this).position().top;var above_screen=img_poswinheight+scrolled_down+preload_threshold;if(!above_screen&&!below_screen){$(this).find('img').attr('src','api/fetchalbumart/?data='+$(this).attr('search-data'));$(this).removeClass('unloaded');}});$(cssSelector).find('.meta-info.unloaded').each(function(idx){var track_pos=$(this).parent().position().top;var above_screen=track_poswinheight+scrolled_down+preload_threshold;if(!above_screen&&!below_screen){var self=this;var path_url_enc=$(this).attr('path');var success=function(data){$(self).show();var metainfo=$.parseJSON(data);if(metainfo.artist.length>0&&metainfo.title.length>0){$(self).find('.meta-info-artist').text(metainfo.artist);$(self).find('.meta-info-title').text(metainfo.title);if(metainfo.track.length>0){if(metainfo.track.length<2){metainfo.track='0'+metainfo.track;} +MediaBrowser.static={_renderList:function(l,listview){"use strict";var self=this;var html="";$.each(l,function(i,e){switch(e.type){case'dir':html+=MediaBrowser.static._renderDirectory(e,listview);break;case'file':html+=MediaBrowser.static._renderFile(e);break;case'compact':html+=MediaBrowser.static._renderCompactDirectory(e);break;case'playlist':html+=MediaBrowser.static._renderPlaylist(e);break;default:window.console.log('cannot render unknown type '+e.type);}});return html;},_renderMessage:function(msg){var template=templateLoader.cached('mediabrowser-message');return Mustache.render(template,{message:msg});},_renderFile:function(json){var template=templateLoader.cached('mediabrowser-file');var template_data={fileurl:json.urlpath,fullpath:json.path,label:json.label,track:json.track,};return Mustache.render(template,template_data);},_renderDirectory:function(json,listview){var template=templateLoader.cached('mediabrowser-directory');var template_data={isrootdir:json.path&&!json.path.indexOf('/')>0,dirpath:json.path,label:json.label,listview:listview,maychangecoverart:!!isAdmin,coverarturl:encodeURIComponent(JSON.stringify({'directory':json.path})),directoryname:encodeURIComponent(json.path),foldercount:json.foldercount,showfoldercount:json.foldercount>0,filescount:json.filescount,showfilescount:json.filescount>0,filescountestimate:json.filescountestimate,};return Mustache.render(template,template_data);},_renderPlaylist:function(e){var template=templateLoader.cached('mediabrowser-playlist');var template_data={playlistid:e['plid'],isowner:e.owner,candelete:e.owner||isAdmin,playlistlabel:e['title'],username:e['username'],age:time2text(e['age']),username_color:userNameToColor(e.username),publicchecked:e['public']?'checked="checked"':'',publiclabelclass:e['public']?'label-success':'label-default',};return Mustache.render(template,template_data);},_renderCompactDirectory:function(json){var template=templateLoader.cached('mediabrowser-compact');var template_data={filepath:json.urlpath,filter:json.label,filterUPPER:json.label.toUpperCase(),};return Mustache.render(template,template_data);},addThisTrackToPlaylist:function(){"use strict" +playlistManager.addSong($(this).attr("path"),$(this).attr("title"),parseInt($(this).attr("track")));$(this).blur();return false;},_addAllToPlaylist:function($source,plid){"use strict";$source.find('li .musicfile').each(function(){playlistManager.addSong($(this).attr("path"),$(this).attr("title"),parseInt($(this).attr("track")),plid);});},albumArtLoader:function(cssSelector){"use strict";var winheight=$(window).height();var scrolled_down=$(cssSelector).scrollTop();var preload_threshold=50;$(cssSelector).find('.list-dir-albumart.unloaded').each(function(idx){var img_pos=$(this).position().top;var above_screen=img_poswinheight+scrolled_down+preload_threshold;if(!above_screen&&!below_screen){$(this).find('img').attr('src','api/fetchalbumart/?data='+$(this).attr('search-data'));$(this).removeClass('unloaded');}});$(cssSelector).find('.meta-info.unloaded').each(function(idx){var track_pos=$(this).parent().position().top;var above_screen=track_poswinheight+scrolled_down+preload_threshold;if(!above_screen&&!below_screen){var self=this;var path_url_enc=$(this).attr('path');var success=function(data){$(self).show();var metainfo=$.parseJSON(data);if(metainfo.artist.length>0&&metainfo.title.length>0){$(self).find('.meta-info-artist').text(metainfo.artist);$(self).find('.meta-info-title').text(metainfo.title);if(metainfo.track.length>0){if(metainfo.track.length<2){metainfo.track='0'+metainfo.track;} $(self).find('.meta-info-track').text(metainfo.track);} $(self).parent().find('.simplelabel').hide();} if(metainfo.length){$(self).find('.meta-info-length').text('('+jPlayerPlaylist.prototype._formatTime(metainfo.length)+')');}} -$(this).removeClass('unloaded');api('getsonginfo',{'path':decodeURIComponent(path_url_enc)},success,errorFunc('error getting song metainfo'),true);}});},};var browser=detectBrowser();if(['msie','safari'].indexOf(browser)!=-1){var encoderPreferenceOrder=['mp3','ogg'];}else{var encoderPreferenceOrder=['ogg','mp3'];} +$(this).removeClass('unloaded');api('getsonginfo',{'path':decodeURIComponent(path_url_enc),'track':parseInt($(self).parent().attr('track'))},success,errorFunc('error getting song metainfo'),true);}});},};var browser=detectBrowser();if(['msie','safari'].indexOf(browser)!=-1){var encoderPreferenceOrder=['mp3','ogg'];}else{var encoderPreferenceOrder=['ogg','mp3'];} var SERVER_CONFIG={};var availableEncoders=undefined;var availablejPlayerFormats=['mp3','ogg'];var availableDecoders=undefined;var transcodingEnabled=undefined;var userOptions=undefined;var isAdmin=undefined;var loggedInUserName=undefined;var REMEMBER_PLAYLIST_INTERVAL=3000;var CHECK_MUSIC_PLAYING_INTERVAL=2000;var HEARTBEAT_INTERVAL_MS=30*1000;var playlistSelector='.jp-playlist';var executeAfterConfigLoaded=[] function api(){"use strict";var action=arguments[0];var has_data=!(typeof arguments[1]==='function');var data={};if(has_data){data=arguments[1];} var successfunc=arguments[has_data?2:1];var errorfunc=arguments[has_data?3:2];var completefunc=arguments[has_data?4:3];if(!successfunc)successfunc=function(){};if(!completefunc)completefunc=function(){};var successFuncWrapper=function(successFunc){return function handler(json){var result=$.parseJSON(json);if(result.flash){successNotify(result.flash);} @@ -1111,11 +1112,11 @@ function ord(c) function showPlaylists(sortby,filterby){"use strict";var success=function(data){var addressAndPort=getAddrPort();var value_before=$('.playlist-filter-input').val();new MediaBrowser('.search-results',data,'Playlist browser',false,{showPlaylistPanel:true});$('.playlist-filter-input').val(value_before);};var error=errorFunc('error loading external playlists');busy('.search-results').hide().fadeIn('fast');api('showplaylists',{'sortby':sortby,'filterby':filterby},success,error,function(){busy('.search-results').fadeOut('fast')});} function changePlaylist(plid,attrname,value){window.console.log(plid);window.console.log(attrname);window.console.log(value);busy('#playlist-panel').hide().fadeIn('fast');api('changeplaylist',{'plid':plid,'attribute':attrname,'value':value},function(){showPlaylists();},errorFunc('error changing playlist attribute'),function(){busy('#playlist-panel').fadeOut('fast')});} function confirmDeletePlaylist(id,title){$('#deletePlaylistConfirmButton').off();$('#deletePlaylistConfirmButton').on('click',function(){busy('#playlist-panel').hide().fadeIn('fast');api('deleteplaylist',{'playlistid':id},false,errorFunc('error deleting playlist'),function(){busy('#playlist-panel').fadeOut('fast')});$('#dialog').fadeOut('fast');showPlaylists();});$('#deleteplaylistmodalLabel').html(Mustache.render('Really delete Playlist "{{title}}"',{title:title}));$('#deleteplaylistmodal').modal('show');} -function loadPlaylist(playlistid,playlistlabel){var success=function(data){var tracklist=data;var pl=playlistManager.newPlaylist([],playlistlabel);var animate=false;for(var i=0;i '+availablejPlayerFormats[i]+' @ '+transurl); } @@ -684,7 +689,7 @@ PlaylistManager.prototype = { } return track; }, - addSong : function(path, title, plid, animate){ + addSong : function(path, title, track_, plid, animate){ "use strict"; var self = this; if(typeof animate === 'undefined'){ @@ -693,6 +698,7 @@ PlaylistManager.prototype = { var track = { title: title, url: path, + track: track_, wasPlayed : 0, } var playlist; @@ -740,7 +746,7 @@ PlaylistManager.prototype = { // for the actual audio data comes through frist window.setTimeout( function(){ - api('getsonginfo', {'path': decodeURIComponent(path)}, success, errorFunc('error getting song metainfo'), true); + api('getsonginfo', {'path': decodeURIComponent(path), 'track': track_}, success, errorFunc('error getting song metainfo'), true); }, 1000 ); diff --git a/res/templates/mediabrowser-file.html b/res/templates/mediabrowser-file.html index e9e79483..1112e9c2 100644 --- a/res/templates/mediabrowser-file.html +++ b/res/templates/mediabrowser-file.html @@ -1,5 +1,5 @@
  • - + {{label}} @@ -13,4 +13,4 @@ {{fullpath}} -
  • \ No newline at end of file + From ab51037e3cc728566919fdbd1cfb124e2e2c5bf2 Mon Sep 17 00:00:00 2001 From: Jon Bergli Heier Date: Wed, 21 May 2014 23:36:19 +0200 Subject: [PATCH 2/8] Pass track in transcoder unittest. --- cherrymusicserver/test/test_httphandler.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cherrymusicserver/test/test_httphandler.py b/cherrymusicserver/test/test_httphandler.py index 420ae4b5..c60159b6 100644 --- a/cherrymusicserver/test/test_httphandler.py +++ b/cherrymusicserver/test/test_httphandler.py @@ -313,7 +313,7 @@ def test_trans(self): httphandler.HTTPHandler(config).trans('newformat', 'path', bitrate=111) - transcoder.transcodeStream.assert_called_with(expectPath, 'newformat', bitrate=111, starttime=0) + transcoder.transcodeStream.assert_called_with(expectPath, 'newformat', bitrate=111, track=None, starttime=0) if __name__ == "__main__": From 4bf1b816cbad4a82b9159d21668039d40aa29578 Mon Sep 17 00:00:00 2001 From: Jon Bergli Heier Date: Thu, 29 May 2014 21:52:30 +0200 Subject: [PATCH 3/8] Replaced track parameter with starttime and duration. Current issues: * For file formats that CM doesn't know about, we don't know the length of the audio file, and thus can't fetch the length of the last track. * We should remove the duration flag when no duration is given, this is currently set to 100000 seconds when duration is None (unspecified). Other notes: * getsonginfo() needs to know which track is being fetched, this is currenly checked by comparing the provided starttime parameter with each track. This could be either left as-is, or done differently. * Decode.decode() must resolve the audio filepath itself, since we use the cue filepath to map to the proper decoder. The cue and audio files can also have different base names (eg. foo.cue can point to bar.flac). * starttime must include decimals to avoid playing back the last second or so of the previous track, duration also does this to stay consistent. --- audiotranscode/__init__.py | 54 ++++++++-------------- cherrymusicserver/cherrymodel.py | 41 ++++++++++++---- cherrymusicserver/httphandler.py | 14 +++--- cherrymusicserver/metainfo.py | 13 +++--- cherrymusicserver/test/test_httphandler.py | 2 +- res/dist/cherrymusic.dist.js | 25 +++++----- res/js/cherrymusic.js | 4 +- res/js/mediabrowser.js | 13 ++++-- res/js/playlistmanager.js | 21 +++++---- res/templates/mediabrowser-file.html | 2 +- 10 files changed, 103 insertions(+), 86 deletions(-) diff --git a/audiotranscode/__init__.py b/audiotranscode/__init__.py index be41bbf5..7a17d462 100644 --- a/audiotranscode/__init__.py +++ b/audiotranscode/__init__.py @@ -53,42 +53,28 @@ def __init__(self, filetype, command): self.filetype = filetype self.mimetype = MimeTypes[filetype] - def decode(self, filepath, starttime=0, track=None): - cmd = self.command[:] + def decode(self, filepath, starttime=0, duration=None): + # Fetch audio filepath from cue sheets ext = os.path.splitext(filepath)[1] if ext == '.cue': - if not track: - track = 1 - else: - track = int(track) from cherrymusicserver.cuesheet import Cuesheet cue = Cuesheet(filepath) - audio_filepath = None for cdtext in cue.info[0].cdtext: if cdtext.type == 'FILE': - # Set the actual filepath from the FILE field - audio_filepath = os.path.join(os.path.dirname(filepath), cdtext.value[0]) + filepath = os.path.join(os.path.dirname(filepath), cdtext.value[0]) break - if audio_filepath is None or not os.path.exists(audio_filepath): - raise DecodeError('Audiofile not found for cuesheet %s'%filepath) - filepath = audio_filepath - track_n = track - track = cue.tracks[track-1] - if 'STARTTIME' in cmd: - cmd[cmd.index('STARTTIME')] = str(track.get_start_time()) - if 'LENGTH' in cmd: - nexttrack = cue.get_next(track) - if nexttrack: - track.nextstart = nexttrack.get_start_time() - cmd[cmd.index('LENGTH')] = str(track.get_length()) - else: - # Last track doesn't have a next track - # XXX: Actually we should remove the -t option altogether here - cmd[cmd.index('LENGTH')] = '99999' - else: - if 'STARTTIME' in cmd: - hours, minutes, seconds = starttime//3600, starttime//60%60, starttime%60 - cmd[cmd.index('STARTTIME')] = '%d:%d:%d' % (hours, minutes, seconds) + cmd = self.command[:] + if 'STARTTIME' in cmd: + hours, minutes, seconds = starttime//3600, starttime//60%60, starttime%60 + # Seconds should include decimals so that multi-track files don't + # accidentally include the last second of the previous track. + cmd[cmd.index('STARTTIME')] = '%d:%d:%f' % (hours, minutes, seconds) + if 'DURATION' in cmd: + # FIXME: We should remove the duration flag instead of working around it like this. + if duration is None: + duration = 100000 + hours, minutes, seconds = duration//3600, duration//60%60, duration%60 + cmd[cmd.index('DURATION')] = '%d:%d:%f' % (hours, minutes, seconds) if 'INPUT' in cmd: cmd[cmd.index('INPUT')] = filepath return subprocess.Popen(cmd, @@ -139,7 +125,7 @@ class AudioTranscode: Decoder('aac' , ['faad', '-w', 'INPUT']), Decoder('m4a' , ['faad', '-w', 'INPUT']), Decoder('wav' , ['cat', 'INPUT']), - Decoder('cue' , ['ffmpeg', '-ss', 'STARTTIME', '-t', 'LENGTH', '-i', 'INPUT', '-f', 'wav', '-']), + Decoder('cue' , ['ffmpeg', '-ss', 'STARTTIME', '-t', 'DURATION', '-i', 'INPUT', '-f', 'wav', '-']), ] def __init__(self,debug=False): @@ -158,7 +144,7 @@ def _filetype(self, filepath): if '.' in filepath: return filepath.lower()[filepath.rindex('.')+1:] - def _decode(self, filepath, decoder=None, starttime=0, track=None): + def _decode(self, filepath, decoder=None, starttime=0, duration=None): if not os.path.exists(filepath): filepath = os.path.abspath(filepath) raise DecodeError('File not Found! Cannot decode "file" %s'%filepath) @@ -172,7 +158,7 @@ def _decode(self, filepath, decoder=None, starttime=0, track=None): break if self.debug: print(decoder) - return decoder.decode(filepath, starttime=starttime, track=track) + return decoder.decode(filepath, starttime=starttime, duration=duration) def _encode(self, audio_format, decoder_process, bitrate=None,encoder=None): if not audio_format in self.availableEncoderFormats(): @@ -199,11 +185,11 @@ def transcode(self, in_file, out_file, bitrate=None): fh.close() def transcodeStream(self, filepath, newformat, bitrate=None, - encoder=None, decoder=None, starttime=0, track=None): + encoder=None, decoder=None, starttime=0, duration=None): decoder_process = None encoder_process = None try: - decoder_process = self._decode(filepath, decoder, starttime=starttime, track=track) + decoder_process = self._decode(filepath, decoder, starttime=starttime, duration=duration) encoder_process = self._encode(newformat, decoder_process,bitrate=bitrate,encoder=encoder) while encoder_process.poll() == None: data = encoder_process.stdout.read(AudioTranscode.READ_BUFFER) diff --git a/cherrymusicserver/cherrymodel.py b/cherrymusicserver/cherrymodel.py index 2af477da..1a4c1b7c 100644 --- a/cherrymusicserver/cherrymodel.py +++ b/cherrymusicserver/cherrymodel.py @@ -167,13 +167,31 @@ def listdir(self, dirpath, filterstr=''): def addCueSheet(cls, filepath, list): from cherrymusicserver.cuesheet import Cuesheet cue = Cuesheet(filepath) - for track in range(len(cue.tracks)): - list.append(MusicEntry(strippath(filepath), track = track+1)) + audio_filepath = None + for cdtext in cue.info[0].cdtext: + if cdtext.type == 'FILE': + # Set the actual filepath from the FILE field + audio_filepath = os.path.join(os.path.dirname(filepath), cdtext.value[0]) + break + if audio_filepath is None or not os.path.exists(audio_filepath): + log.info(_("Could not find a valid audio file path in cue sheet '%(filepath)'", {'filepath': filepath})) + return + for track in cue.tracks: + starttime = track.get_start_time() + print('starttime:', starttime) + # We need to know the length of the audio file to get the duration of the last track. + nexttrack = cue.get_next(track) + if nexttrack: + track.nextstart = nexttrack.get_start_time() + duration = track.get_length() + else: + duration = None + list.append(MusicEntry(strippath(filepath), starttime = starttime, duration = duration)) @classmethod def addMusicEntry(cls, fullpath, list): if os.path.isfile(fullpath): - if iscuesheet(fullpath): + if CherryModel.iscuesheet(fullpath): CherryModel.addCueSheet(fullpath, list) elif CherryModel.isplayable(fullpath): list.append(MusicEntry(strippath(fullpath))) @@ -322,9 +340,10 @@ def isplayable(cls, filename): is_empty_file = os.path.getsize(CherryModel.abspath(filename)) == 0 return is_supported_ext and not is_empty_file -def iscuesheet(filename): - ext = os.path.splitext(filename)[1] - return ext.lower() == '.cue' + @staticmethod + def iscuesheet(filename): + ext = os.path.splitext(filename)[1] + return ext.lower() == '.cue' def strippath(path): if path.startswith(cherry.config['media.basedir']): @@ -337,7 +356,7 @@ class MusicEntry: # check if there are playable meadia files or other folders inside MAX_SUB_FILES_ITER_COUNT = 100 - def __init__(self, path, compact=False, dir=False, repr=None, subdircount=0, subfilescount=0, track=None): + def __init__(self, path, compact=False, dir=False, repr=None, subdircount=0, subfilescount=0, starttime=None, duration=None): self.path = path self.compact = compact self.dir = dir @@ -348,8 +367,9 @@ def __init__(self, path, compact=False, dir=False, repr=None, subdircount=0, sub self.subfilescount = subfilescount # True when the exact amount of files is too big and is estimated self.subfilesestimate = False - # Track number for multi-track files - self.track = track + # Times for start and length of the track + self.starttime = starttime + self.duration = duration def count_subfolders_and_files(self): if self.dir: @@ -395,7 +415,8 @@ def to_dict(self): return {'type': 'file', 'urlpath': urlpath, 'path': self.path, - 'track': self.track, + 'starttime': self.starttime, + 'duration': self.duration, 'label': simplename} def __repr__(self): diff --git a/cherrymusicserver/httphandler.py b/cherrymusicserver/httphandler.py index 35f30025..7f7fd51c 100644 --- a/cherrymusicserver/httphandler.py +++ b/cherrymusicserver/httphandler.py @@ -252,18 +252,18 @@ def trans(self, newformat, *path, **params): path = codecs.decode(codecs.encode(path, 'latin1'), 'utf-8') fullpath = os.path.join(cherry.config['media.basedir'], path) - starttime = int(params.pop('starttime', 0)) - if 'track' in params: - track = params.pop('track') + starttime = float(params.pop('starttime', 0)) + if 'duration' in params: + duration = float(params.pop('duration')) else: - track = None + duration = None transcoder = audiotranscode.AudioTranscode() mimetype = transcoder.mimeType(newformat) cherrypy.response.headers["Content-Type"] = mimetype try: return transcoder.transcodeStream(fullpath, newformat, - bitrate=bitrate, starttime=starttime, track=track) + bitrate=bitrate, starttime=starttime, duration=duration) except audiotranscode.TranscodeError as e: raise cherrypy.HTTPError(404, e.value) trans.exposed = True @@ -656,10 +656,10 @@ def export_playlists(self, format, all=False, hostaddr=''): return zip.getbytes() export_playlists.exposed = True - def api_getsonginfo(self, path, track = None): + def api_getsonginfo(self, path, starttime=None): basedir = cherry.config['media.basedir'] abspath = os.path.join(basedir, path) - return json.dumps(metainfo.getSongInfo(abspath, track).dict()) + return json.dumps(metainfo.getSongInfo(abspath, starttime).dict()) def api_getencoders(self): return json.dumps(audiotranscode.getEncoders()) diff --git a/cherrymusicserver/metainfo.py b/cherrymusicserver/metainfo.py index 515a29ba..556ab0e8 100644 --- a/cherrymusicserver/metainfo.py +++ b/cherrymusicserver/metainfo.py @@ -50,20 +50,21 @@ def dict(self): 'length': self.length } -def getSongInfo(filepath, _track=None): +def getSongInfo(filepath, starttime=None): ext = os.path.splitext(filepath)[1] - if ext == '.cue' and _track: + if ext == '.cue' and starttime is not None: from cherrymusicserver.cuesheet import Cuesheet cue = Cuesheet(filepath) info = cue.info[0] artist = info.performer or '-' album = info.title or '-' title = '-' - _track = int(_track) - track = cue.tracks[_track-1] + for track_n, track in enumerate(cue.tracks, 1): + if track.get_start_time() >= starttime: + break artist = track.performer or artist title = track.title or title - if _track < len(cue.tracks): + if track_n < len(cue.tracks): track.nextstart = cue.get_next(track).get_start_time() audiolength = track.get_length() else: @@ -77,7 +78,7 @@ def getSongInfo(filepath, _track=None): audiolength = 0 else: audiolength = tag.duration - return Metainfo(artist, album, title, _track, audiolength) + return Metainfo(artist, album, title, track_n, audiolength) try: tag = TinyTag.get(filepath) except LookupError: diff --git a/cherrymusicserver/test/test_httphandler.py b/cherrymusicserver/test/test_httphandler.py index c60159b6..ba18823f 100644 --- a/cherrymusicserver/test/test_httphandler.py +++ b/cherrymusicserver/test/test_httphandler.py @@ -313,7 +313,7 @@ def test_trans(self): httphandler.HTTPHandler(config).trans('newformat', 'path', bitrate=111) - transcoder.transcodeStream.assert_called_with(expectPath, 'newformat', bitrate=111, track=None, starttime=0) + transcoder.transcodeStream.assert_called_with(expectPath, 'newformat', bitrate=111, starttime=0, duration=None) if __name__ == "__main__": diff --git a/res/dist/cherrymusic.dist.js b/res/dist/cherrymusic.dist.js index 67727be9..15a4085f 100644 --- a/res/dist/cherrymusic.dist.js +++ b/res/dist/cherrymusic.dist.js @@ -972,7 +972,7 @@ return this} $(document).on('click.bs.alert.data-api',dismiss,Alert.prototype.close)}(window.jQuery);;var ManagedPlaylist=function(playlistManager,playlist,options){this.playlistManager=playlistManager;this.id=options.id;this.name=options.name;this.closable=options.closable;this.public=options.public;this.owner=options.owner;this.saved=options.saved;this.reason_open=options.reason_open;this.jplayerplaylist;this._init(playlist,playlistManager)} ManagedPlaylist.prototype={_init:function(playlist,playlistManager){var self=this;this.playlistSelector=self._createNewPlaylistContainer();for(var i=0;iul.playlist-container-list").sortable({axis:"y",delay:150,helper:'clone',update:function(e,ui){self.jplayerplaylist.scan();}});$(self.playlistSelector+">ul.playlist-container-list").disableSelection();$(this.playlistSelector).bind('requestPlay',function(event,playlistSelector){self.playlistManager.setPlayingPlaylist(self.playlistManager.htmlid2plid(playlistSelector));});$(this.playlistSelector).bind('removedItem',function(event,playlistSelector){self.setSaved(false);});$(this.playlistSelector).bind('sortedItems',function(event,playlistSelector){self.setSaved(false);});$(this.playlistSelector).bind('addedItem',function(event,playlistSelector){self.setSaved(false);});},setSaved:function(issaved){this.saved=issaved;this.playlistManager.refreshCommands();this.playlistManager.refreshTabs();},wasSaved:function(){return this.saved;},_createNewPlaylistContainer:function(){var playlistContainerParent=this.playlistManager.cssSelectorPlaylistContainerParent;var id=this.playlistManager.plid2htmlid(this.id);$(playlistContainerParent).append('');return'#'+id;},getCanonicalPlaylist:function(){var canonical=[];for(var i=0;iul.playlist-container-list").sortable({axis:"y",delay:150,helper:'clone',update:function(e,ui){self.jplayerplaylist.scan();}});$(self.playlistSelector+">ul.playlist-container-list").disableSelection();$(this.playlistSelector).bind('requestPlay',function(event,playlistSelector){self.playlistManager.setPlayingPlaylist(self.playlistManager.htmlid2plid(playlistSelector));});$(this.playlistSelector).bind('removedItem',function(event,playlistSelector){self.setSaved(false);});$(this.playlistSelector).bind('sortedItems',function(event,playlistSelector){self.setSaved(false);});$(this.playlistSelector).bind('addedItem',function(event,playlistSelector){self.setSaved(false);});},setSaved:function(issaved){this.saved=issaved;this.playlistManager.refreshCommands();this.playlistManager.refreshTabs();},wasSaved:function(){return this.saved;},_createNewPlaylistContainer:function(){var playlistContainerParent=this.playlistManager.cssSelectorPlaylistContainerParent;var id=this.playlistManager.plid2htmlid(this.id);$(playlistContainerParent).append('');return'#'+id;},getCanonicalPlaylist:function(){var canonical=[];for(var i=0;iwasplayermost){wasplayermost=this.jplayerplaylist.playlist[i].wasPlayed;}} @@ -1007,13 +1007,14 @@ var otherId=this.managedPlaylists[i '+availablejPlayerFormats[i]+' @ '+transurl);}}} +return;}else{for(var i=0;i '+availablejPlayerFormats[i]+' @ '+transurl);}}} if(formats.length==0){window.console.log('no suitable encoder available! Try installing vorbis-tools or lame!');return;}} -return track;},addSong:function(path,title,track_,plid,animate){"use strict";var self=this;if(typeof animate==='undefined'){animate=true;} -var track={title:title,url:path,track:track_,wasPlayed:0,} +return track;},addSong:function(path,title,starttime,duration,plid,animate){"use strict";var self=this;if(typeof animate==='undefined'){animate=true;} +var track={title:title,url:path,starttime:starttime,duration:duration,wasPlayed:0,} var playlist;if(plid){playlist=this.getPlaylistById(plid);} if(typeof playlist=='undefined'){playlist=this.getEditingPlaylist();} playlist.addTrack(track,animate);if(!jPlayerIsPlaying()&&playlist.jplayerplaylist.playlist.length==1){if(userOptions.misc.autoplay_on_add){playlist.makeThisPlayingPlaylist();playlist.jplayerplaylist.play(0);}else{playlist.jplayerplaylist.select(0);}} @@ -1021,7 +1022,7 @@ var success=function(data){var metainfo=$.parseJSON(data);var any_info_received= if(metainfo.title.length>0&&metainfo.artist.length>0){track.title=metainfo.artist+' - '+metainfo.title;if(metainfo.track.length>0){track.title=metainfo.track+' '+track.title;if(metainfo.track.length<2){track.title='0'+track.title;}} any_info_received=true;} if(any_info_received){self.getEditingPlaylist().jplayerplaylist._refresh(true);}} -window.setTimeout(function(){api('getsonginfo',{'path':decodeURIComponent(path),'track':track_},success,errorFunc('error getting song metainfo'),true);},1000);},clearPlaylist:function(){"use strict";this.getEditingPlaylist().remove();if(this.getEditingPlaylist()==this.getPlayingPlaylist()){$(this.cssSelectorjPlayer).jPlayer("clearMedia");} +window.setTimeout(function(){api('getsonginfo',{'path':decodeURIComponent(path),'starttime':starttime},success,errorFunc('error getting song metainfo'),true);},1000);},clearPlaylist:function(){"use strict";this.getEditingPlaylist().remove();if(this.getEditingPlaylist()==this.getPlayingPlaylist()){$(this.cssSelectorjPlayer).jPlayer("clearMedia");} return false;},displayCurrentSong:function(){var pl=this.getPlayingPlaylist();if(typeof pl==='undefined'){return;} var jPlaylist=pl.jplayerplaylist;var songtitle='';var tabtitle='CherryMusic';if(typeof this.jPlayerInstance!=='undefined'){var currentTitle=this.jPlayerInstance.data().jPlayer.status.media.title;if(typeof currentTitle!=='undefined'){songtitle=currentTitle;tabtitle=currentTitle+' | CherryMusic';}} $('.cm-songtitle').html(songtitle);$('title').text(tabtitle);},rememberPlaylist:function(){"use strict";var self=this;var canonicalPlaylists=[] @@ -1059,12 +1060,12 @@ this.render();$(cssSelector).off('click');$(cssSelector).on('click','.list-dir', $('#changeAlbumArt').modal('show');});$(cssSelector).on('click','.mb-view-list-enable',view_list_enable);$(cssSelector).on('click','.mb-view-cover-enable',view_cover_enable);$(cssSelector).on('click','.addAllToPlaylist',function(){if(isplaylist){var pl=playlistManager.newPlaylist([],playlistlabel);}else{var pl=playlistManager.getEditingPlaylist();} MediaBrowser.static._addAllToPlaylist($(this),pl.id);if(isplaylist){pl.setSaved(true);} $(this).blur();return false;});} -MediaBrowser.static={_renderList:function(l,listview){"use strict";var self=this;var html="";$.each(l,function(i,e){switch(e.type){case'dir':html+=MediaBrowser.static._renderDirectory(e,listview);break;case'file':html+=MediaBrowser.static._renderFile(e);break;case'compact':html+=MediaBrowser.static._renderCompactDirectory(e);break;case'playlist':html+=MediaBrowser.static._renderPlaylist(e);break;default:window.console.log('cannot render unknown type '+e.type);}});return html;},_renderMessage:function(msg){var template=templateLoader.cached('mediabrowser-message');return Mustache.render(template,{message:msg});},_renderFile:function(json){var template=templateLoader.cached('mediabrowser-file');var template_data={fileurl:json.urlpath,fullpath:json.path,label:json.label,track:json.track,};return Mustache.render(template,template_data);},_renderDirectory:function(json,listview){var template=templateLoader.cached('mediabrowser-directory');var template_data={isrootdir:json.path&&!json.path.indexOf('/')>0,dirpath:json.path,label:json.label,listview:listview,maychangecoverart:!!isAdmin,coverarturl:encodeURIComponent(JSON.stringify({'directory':json.path})),directoryname:encodeURIComponent(json.path),foldercount:json.foldercount,showfoldercount:json.foldercount>0,filescount:json.filescount,showfilescount:json.filescount>0,filescountestimate:json.filescountestimate,};return Mustache.render(template,template_data);},_renderPlaylist:function(e){var template=templateLoader.cached('mediabrowser-playlist');var template_data={playlistid:e['plid'],isowner:e.owner,candelete:e.owner||isAdmin,playlistlabel:e['title'],username:e['username'],age:time2text(e['age']),username_color:userNameToColor(e.username),publicchecked:e['public']?'checked="checked"':'',publiclabelclass:e['public']?'label-success':'label-default',};return Mustache.render(template,template_data);},_renderCompactDirectory:function(json){var template=templateLoader.cached('mediabrowser-compact');var template_data={filepath:json.urlpath,filter:json.label,filterUPPER:json.label.toUpperCase(),};return Mustache.render(template,template_data);},addThisTrackToPlaylist:function(){"use strict" -playlistManager.addSong($(this).attr("path"),$(this).attr("title"),parseInt($(this).attr("track")));$(this).blur();return false;},_addAllToPlaylist:function($source,plid){"use strict";$source.find('li .musicfile').each(function(){playlistManager.addSong($(this).attr("path"),$(this).attr("title"),parseInt($(this).attr("track")),plid);});},albumArtLoader:function(cssSelector){"use strict";var winheight=$(window).height();var scrolled_down=$(cssSelector).scrollTop();var preload_threshold=50;$(cssSelector).find('.list-dir-albumart.unloaded').each(function(idx){var img_pos=$(this).position().top;var above_screen=img_poswinheight+scrolled_down+preload_threshold;if(!above_screen&&!below_screen){$(this).find('img').attr('src','api/fetchalbumart/?data='+$(this).attr('search-data'));$(this).removeClass('unloaded');}});$(cssSelector).find('.meta-info.unloaded').each(function(idx){var track_pos=$(this).parent().position().top;var above_screen=track_poswinheight+scrolled_down+preload_threshold;if(!above_screen&&!below_screen){var self=this;var path_url_enc=$(this).attr('path');var success=function(data){$(self).show();var metainfo=$.parseJSON(data);if(metainfo.artist.length>0&&metainfo.title.length>0){$(self).find('.meta-info-artist').text(metainfo.artist);$(self).find('.meta-info-title').text(metainfo.title);if(metainfo.track.length>0){if(metainfo.track.length<2){metainfo.track='0'+metainfo.track;} +MediaBrowser.static={_renderList:function(l,listview){"use strict";var self=this;var html="";$.each(l,function(i,e){switch(e.type){case'dir':html+=MediaBrowser.static._renderDirectory(e,listview);break;case'file':html+=MediaBrowser.static._renderFile(e);break;case'compact':html+=MediaBrowser.static._renderCompactDirectory(e);break;case'playlist':html+=MediaBrowser.static._renderPlaylist(e);break;default:window.console.log('cannot render unknown type '+e.type);}});return html;},_renderMessage:function(msg){var template=templateLoader.cached('mediabrowser-message');return Mustache.render(template,{message:msg});},_renderFile:function(json){var template=templateLoader.cached('mediabrowser-file');var template_data={fileurl:json.urlpath,fullpath:json.path,label:json.label,starttime:json.starttime,duration:json.duration};return Mustache.render(template,template_data);},_renderDirectory:function(json,listview){var template=templateLoader.cached('mediabrowser-directory');var template_data={isrootdir:json.path&&!json.path.indexOf('/')>0,dirpath:json.path,label:json.label,listview:listview,maychangecoverart:!!isAdmin,coverarturl:encodeURIComponent(JSON.stringify({'directory':json.path})),directoryname:encodeURIComponent(json.path),foldercount:json.foldercount,showfoldercount:json.foldercount>0,filescount:json.filescount,showfilescount:json.filescount>0,filescountestimate:json.filescountestimate,};return Mustache.render(template,template_data);},_renderPlaylist:function(e){var template=templateLoader.cached('mediabrowser-playlist');var template_data={playlistid:e['plid'],isowner:e.owner,candelete:e.owner||isAdmin,playlistlabel:e['title'],username:e['username'],age:time2text(e['age']),username_color:userNameToColor(e.username),publicchecked:e['public']?'checked="checked"':'',publiclabelclass:e['public']?'label-success':'label-default',};return Mustache.render(template,template_data);},_renderCompactDirectory:function(json){var template=templateLoader.cached('mediabrowser-compact');var template_data={filepath:json.urlpath,filter:json.label,filterUPPER:json.label.toUpperCase(),};return Mustache.render(template,template_data);},addThisTrackToPlaylist:function(){"use strict" +playlistManager.addSong($(this).attr("path"),$(this).attr("title"),$(this).attr("starttime"),$(this).attr("duration"));$(this).blur();return false;},_addAllToPlaylist:function($source,plid){"use strict";$source.find('li .musicfile').each(function(){playlistManager.addSong($(this).attr("path"),$(this).attr("title"),$(this).attr("starttime"),$(this).attr("duration"),plid);});},albumArtLoader:function(cssSelector){"use strict";var winheight=$(window).height();var scrolled_down=$(cssSelector).scrollTop();var preload_threshold=50;$(cssSelector).find('.list-dir-albumart.unloaded').each(function(idx){var img_pos=$(this).position().top;var above_screen=img_poswinheight+scrolled_down+preload_threshold;if(!above_screen&&!below_screen){$(this).find('img').attr('src','api/fetchalbumart/?data='+$(this).attr('search-data'));$(this).removeClass('unloaded');}});$(cssSelector).find('.meta-info.unloaded').each(function(idx){var track_pos=$(this).parent().position().top;var above_screen=track_poswinheight+scrolled_down+preload_threshold;if(!above_screen&&!below_screen){var self=this;var path_url_enc=$(this).attr('path');var success=function(data){$(self).show();var metainfo=$.parseJSON(data);if(metainfo.artist.length>0&&metainfo.title.length>0){$(self).find('.meta-info-artist').text(metainfo.artist);$(self).find('.meta-info-title').text(metainfo.title);if(metainfo.track.length>0){if(metainfo.track.length<2){metainfo.track='0'+metainfo.track;} $(self).find('.meta-info-track').text(metainfo.track);} $(self).parent().find('.simplelabel').hide();} if(metainfo.length){$(self).find('.meta-info-length').text('('+jPlayerPlaylist.prototype._formatTime(metainfo.length)+')');}} -$(this).removeClass('unloaded');api('getsonginfo',{'path':decodeURIComponent(path_url_enc),'track':parseInt($(self).parent().attr('track'))},success,errorFunc('error getting song metainfo'),true);}});},};var browser=detectBrowser();if(['msie','safari'].indexOf(browser)!=-1){var encoderPreferenceOrder=['mp3','ogg'];}else{var encoderPreferenceOrder=['ogg','mp3'];} +$(this).removeClass('unloaded');api('getsonginfo',{'path':decodeURIComponent(path_url_enc),'starttime':$(self).parent().attr('starttime')},success,errorFunc('error getting song metainfo'),true);}});},};var browser=detectBrowser();if(['msie','safari'].indexOf(browser)!=-1){var encoderPreferenceOrder=['mp3','ogg'];}else{var encoderPreferenceOrder=['ogg','mp3'];} var SERVER_CONFIG={};var availableEncoders=undefined;var availablejPlayerFormats=['mp3','ogg'];var availableDecoders=undefined;var transcodingEnabled=undefined;var userOptions=undefined;var isAdmin=undefined;var loggedInUserName=undefined;var REMEMBER_PLAYLIST_INTERVAL=3000;var CHECK_MUSIC_PLAYING_INTERVAL=2000;var HEARTBEAT_INTERVAL_MS=30*1000;var playlistSelector='.jp-playlist';var executeAfterConfigLoaded=[] function api(){"use strict";var action=arguments[0];var has_data=!(typeof arguments[1]==='function');var data={};if(has_data){data=arguments[1];} var successfunc=arguments[has_data?2:1];var errorfunc=arguments[has_data?3:2];var completefunc=arguments[has_data?4:3];if(!successfunc)successfunc=function(){};if(!completefunc)completefunc=function(){};var successFuncWrapper=function(successFunc){return function handler(json){var result=$.parseJSON(json);if(result.flash){successNotify(result.flash);} @@ -1112,11 +1113,11 @@ function ord(c) function showPlaylists(sortby,filterby){"use strict";var success=function(data){var addressAndPort=getAddrPort();var value_before=$('.playlist-filter-input').val();new MediaBrowser('.search-results',data,'Playlist browser',false,{showPlaylistPanel:true});$('.playlist-filter-input').val(value_before);};var error=errorFunc('error loading external playlists');busy('.search-results').hide().fadeIn('fast');api('showplaylists',{'sortby':sortby,'filterby':filterby},success,error,function(){busy('.search-results').fadeOut('fast')});} function changePlaylist(plid,attrname,value){window.console.log(plid);window.console.log(attrname);window.console.log(value);busy('#playlist-panel').hide().fadeIn('fast');api('changeplaylist',{'plid':plid,'attribute':attrname,'value':value},function(){showPlaylists();},errorFunc('error changing playlist attribute'),function(){busy('#playlist-panel').fadeOut('fast')});} function confirmDeletePlaylist(id,title){$('#deletePlaylistConfirmButton').off();$('#deletePlaylistConfirmButton').on('click',function(){busy('#playlist-panel').hide().fadeIn('fast');api('deleteplaylist',{'playlistid':id},false,errorFunc('error deleting playlist'),function(){busy('#playlist-panel').fadeOut('fast')});$('#dialog').fadeOut('fast');showPlaylists();});$('#deleteplaylistmodalLabel').html(Mustache.render('Really delete Playlist "{{title}}"',{title:title}));$('#deleteplaylistmodal').modal('show');} -function loadPlaylist(playlistid,playlistlabel){var success=function(data){var tracklist=data;var pl=playlistManager.newPlaylist([],playlistlabel);var animate=false;for(var i=0;i '+availablejPlayerFormats[i]+' @ '+transurl); } @@ -689,7 +693,7 @@ PlaylistManager.prototype = { } return track; }, - addSong : function(path, title, track_, plid, animate){ + addSong : function(path, title, starttime, duration, plid, animate){ "use strict"; var self = this; if(typeof animate === 'undefined'){ @@ -698,7 +702,8 @@ PlaylistManager.prototype = { var track = { title: title, url: path, - track: track_, + starttime: starttime, + duration: duration, wasPlayed : 0, } var playlist; @@ -746,7 +751,7 @@ PlaylistManager.prototype = { // for the actual audio data comes through frist window.setTimeout( function(){ - api('getsonginfo', {'path': decodeURIComponent(path), 'track': track_}, success, errorFunc('error getting song metainfo'), true); + api('getsonginfo', {'path': decodeURIComponent(path), 'starttime': starttime}, success, errorFunc('error getting song metainfo'), true); }, 1000 ); diff --git a/res/templates/mediabrowser-file.html b/res/templates/mediabrowser-file.html index 1112e9c2..f36a32a0 100644 --- a/res/templates/mediabrowser-file.html +++ b/res/templates/mediabrowser-file.html @@ -1,5 +1,5 @@
  • - + {{label}} From 73f97cd16c43c521a8c98b6abe71d33417bb6815 Mon Sep 17 00:00:00 2001 From: Jon Bergli Heier Date: Sat, 30 Aug 2014 21:16:34 +0200 Subject: [PATCH 4/8] Pass metainfo from listdir between mediabrowser and playlist. This allows passing of metainfo for cuesheets without calling getsonginfo for every track. This gets rid of the starttime argument to getsonginfo, and allows us to pass the metainfo we already have when listing cuesheet contents without having to parse the cuesheet for every track. getsonginfo should now be called only once for all tracks. --- cherrymusicserver/cherrymodel.py | 13 ++++--- cherrymusicserver/httphandler.py | 4 +-- cherrymusicserver/metainfo.py | 51 +++++++++++++--------------- res/dist/cherrymusic.dist.js | 20 ++++++----- res/js/cherrymusic.js | 6 ++-- res/js/mediabrowser.js | 43 ++++++++++++++++++++--- res/js/playlistmanager.js | 47 ++++++++++--------------- res/templates/mediabrowser-file.html | 10 +++--- 8 files changed, 111 insertions(+), 83 deletions(-) diff --git a/cherrymusicserver/cherrymodel.py b/cherrymusicserver/cherrymodel.py index 1a4c1b7c..5f96301f 100644 --- a/cherrymusicserver/cherrymodel.py +++ b/cherrymusicserver/cherrymodel.py @@ -166,6 +166,7 @@ def listdir(self, dirpath, filterstr=''): @classmethod def addCueSheet(cls, filepath, list): from cherrymusicserver.cuesheet import Cuesheet + from cherrymusicserver.metainfo import getCueSongInfo cue = Cuesheet(filepath) audio_filepath = None for cdtext in cue.info[0].cdtext: @@ -176,17 +177,19 @@ def addCueSheet(cls, filepath, list): if audio_filepath is None or not os.path.exists(audio_filepath): log.info(_("Could not find a valid audio file path in cue sheet '%(filepath)'", {'filepath': filepath})) return - for track in cue.tracks: + for track_n, track in enumerate(cue.tracks, 1): starttime = track.get_start_time() - print('starttime:', starttime) # We need to know the length of the audio file to get the duration of the last track. nexttrack = cue.get_next(track) + metainfo = getCueSongInfo(filepath, cue, track_n) if nexttrack: track.nextstart = nexttrack.get_start_time() duration = track.get_length() + elif metainfo and metainfo.length: + duration = metainfo.length else: duration = None - list.append(MusicEntry(strippath(filepath), starttime = starttime, duration = duration)) + list.append(MusicEntry(strippath(filepath), starttime = starttime, duration = duration, metainfo = metainfo)) @classmethod def addMusicEntry(cls, fullpath, list): @@ -356,7 +359,7 @@ class MusicEntry: # check if there are playable meadia files or other folders inside MAX_SUB_FILES_ITER_COUNT = 100 - def __init__(self, path, compact=False, dir=False, repr=None, subdircount=0, subfilescount=0, starttime=None, duration=None): + def __init__(self, path, compact=False, dir=False, repr=None, subdircount=0, subfilescount=0, starttime=None, duration=None, metainfo=None): self.path = path self.compact = compact self.dir = dir @@ -370,6 +373,7 @@ def __init__(self, path, compact=False, dir=False, repr=None, subdircount=0, sub # Times for start and length of the track self.starttime = starttime self.duration = duration + self.metainfo = metainfo def count_subfolders_and_files(self): if self.dir: @@ -417,6 +421,7 @@ def to_dict(self): 'path': self.path, 'starttime': self.starttime, 'duration': self.duration, + 'metainfo': self.metainfo.dict() if self.metainfo is not None else None, 'label': simplename} def __repr__(self): diff --git a/cherrymusicserver/httphandler.py b/cherrymusicserver/httphandler.py index 7f7fd51c..d9ede2b2 100644 --- a/cherrymusicserver/httphandler.py +++ b/cherrymusicserver/httphandler.py @@ -656,10 +656,10 @@ def export_playlists(self, format, all=False, hostaddr=''): return zip.getbytes() export_playlists.exposed = True - def api_getsonginfo(self, path, starttime=None): + def api_getsonginfo(self, path): basedir = cherry.config['media.basedir'] abspath = os.path.join(basedir, path) - return json.dumps(metainfo.getSongInfo(abspath, starttime).dict()) + return json.dumps(metainfo.getSongInfo(abspath).dict()) def api_getencoders(self): return json.dumps(audiotranscode.getEncoders()) diff --git a/cherrymusicserver/metainfo.py b/cherrymusicserver/metainfo.py index 556ab0e8..c5a53cd7 100644 --- a/cherrymusicserver/metainfo.py +++ b/cherrymusicserver/metainfo.py @@ -50,35 +50,30 @@ def dict(self): 'length': self.length } -def getSongInfo(filepath, starttime=None): - ext = os.path.splitext(filepath)[1] - if ext == '.cue' and starttime is not None: - from cherrymusicserver.cuesheet import Cuesheet - cue = Cuesheet(filepath) - info = cue.info[0] - artist = info.performer or '-' - album = info.title or '-' - title = '-' - for track_n, track in enumerate(cue.tracks, 1): - if track.get_start_time() >= starttime: - break - artist = track.performer or artist - title = track.title or title - if track_n < len(cue.tracks): - track.nextstart = cue.get_next(track).get_start_time() - audiolength = track.get_length() +def getCueSongInfo(filepath, cue, track_n): + info = cue.info[0] + artist = info.performer or '-' + album = info.title or '-' + title = '-' + track = cue.tracks[track_n-1] + artist = track.performer or artist + title = track.title or title + if track_n < len(cue.tracks): + track.nextstart = cue.get_next(track).get_start_time() + audiolength = track.get_length() + else: + try: + audiofilepath = os.path.join(os.path.dirname(filepath), info.file[0]) + tag = TinyTag.get(audiofilepath) + except Exception: + audiolength = 0 + log.warn('Couldn\'t get length of "%s", setting 0', filepath) else: - # FIXME - try: - #with audioread.audio_open(info.file[0]) as f: - # lasttrack.nextstart = f.duration - audiofilepath = os.path.join(os.path.dirname(filepath), info.file[0]) - tag = TinyTag.get(audiofilepath) - except Exception: - audiolength = 0 - else: - audiolength = tag.duration - return Metainfo(artist, album, title, track_n, audiolength) + audiolength = tag.duration + return Metainfo(artist, album, title, track_n, audiolength) + +def getSongInfo(filepath): + ext = os.path.splitext(filepath)[1] try: tag = TinyTag.get(filepath) except LookupError: diff --git a/res/dist/cherrymusic.dist.js b/res/dist/cherrymusic.dist.js index a3279efc..d4f56ac8 100644 --- a/res/dist/cherrymusic.dist.js +++ b/res/dist/cherrymusic.dist.js @@ -1144,16 +1144,15 @@ return;}else{for(var i=0;i '+availablejPlayerFormats[i]+' @ '+transurl);}}} if(formats.length==0){window.console.log('no suitable encoder available! Try installing vorbis-tools or lame!');return;}} -return track;},addSong:function(path,title,starttime,duration,plid,animate){"use strict";var self=this;if(typeof animate==='undefined'){animate=true;} +return track;},addSong:function(path,title,metainfo,starttime,duration,plid,animate){"use strict";var self=this;if(typeof animate==='undefined'){animate=true;} var track={title:title,url:path,starttime:starttime,duration:duration,wasPlayed:0,} var playlist;if(plid){playlist=this.getPlaylistById(plid);} if(typeof playlist=='undefined'){playlist=this.getEditingPlaylist();} playlist.addTrack(track,animate);if(!jPlayerIsPlaying()&&playlist.jplayerplaylist.playlist.length==1){if(userOptions.misc.autoplay_on_add){playlist.makeThisPlayingPlaylist();playlist.jplayerplaylist.play(0);}else{playlist.jplayerplaylist.select(0);}} -var success=function(data){var metainfo=$.parseJSON(data);var any_info_received=false;if(metainfo.length){track.duration=metainfo.length;any_info_received=true;} +var any_info_received=false;if(metainfo.length){track.duration=metainfo.length;any_info_received=true;} if(metainfo.title.length>0&&metainfo.artist.length>0){track.title=metainfo.artist+' - '+metainfo.title;if(metainfo.track.length>0){track.title=metainfo.track+' '+track.title;if(metainfo.track.length<2){track.title='0'+track.title;}} any_info_received=true;} -if(any_info_received){self.getEditingPlaylist().jplayerplaylist._refresh(true);}} -window.setTimeout(function(){api('getsonginfo',{'path':decodeURIComponent(path),'starttime':starttime},success,errorFunc('error getting song metainfo'),true);},1000);},clearPlaylist:function(){"use strict";this.getEditingPlaylist().remove();if(this.getEditingPlaylist()==this.getPlayingPlaylist()){$(this.cssSelectorjPlayer).jPlayer("clearMedia");} +if(any_info_received){self.getEditingPlaylist().jplayerplaylist._refresh(true);}},clearPlaylist:function(){"use strict";this.getEditingPlaylist().remove();if(this.getEditingPlaylist()==this.getPlayingPlaylist()){$(this.cssSelectorjPlayer).jPlayer("clearMedia");} return false;},displayCurrentSong:function(){var pl=this.getPlayingPlaylist();if(typeof pl==='undefined'){return;} var jPlaylist=pl.jplayerplaylist;var songtitle='';var tabtitle='CherryMusic';if(typeof this.jPlayerInstance!=='undefined'){var currentTitle=this.jPlayerInstance.data().jPlayer.status.media.title;if(typeof currentTitle!=='undefined'){songtitle=currentTitle;tabtitle=currentTitle+' | CherryMusic';}} $('.cm-songtitle').html(songtitle);$('title').text(tabtitle);},rememberPlaylist:function(){"use strict";var self=this;var canonicalPlaylists=[] @@ -1191,12 +1190,15 @@ this.render();$(cssSelector).off('click');$(cssSelector).on('click','.list-dir', $('#changeAlbumArt').modal('show');});$(cssSelector).on('click','.mb-view-list-enable',view_list_enable);$(cssSelector).on('click','.mb-view-cover-enable',view_cover_enable);$(cssSelector).on('click','.addAllToPlaylist',function(){if(isplaylist){var pl=playlistManager.newPlaylist([],playlistlabel);}else{var pl=playlistManager.getEditingPlaylist();} MediaBrowser.static._addAllToPlaylist($(this),pl.id);if(isplaylist){pl.setSaved(true);} $(this).blur();return false;});} -MediaBrowser.static={_renderList:function(l,listview){"use strict";var self=this;var html="";$.each(l,function(i,e){switch(e.type){case'dir':html+=MediaBrowser.static._renderDirectory(e,listview);break;case'file':html+=MediaBrowser.static._renderFile(e);break;case'compact':html+=MediaBrowser.static._renderCompactDirectory(e);break;case'playlist':html+=MediaBrowser.static._renderPlaylist(e);break;default:window.console.log('cannot render unknown type '+e.type);}});return html;},_renderMessage:function(msg){var template=templateLoader.cached('mediabrowser-message');return Mustache.render(template,{message:msg});},_renderFile:function(json){var template=templateLoader.cached('mediabrowser-file');var template_data={fileurl:json.urlpath,fullpath:json.path,label:json.label,starttime:json.starttime,duration:json.duration};return Mustache.render(template,template_data);},_renderDirectory:function(json,listview){var template=templateLoader.cached('mediabrowser-directory');var template_data={isrootdir:json.path&&!json.path.indexOf('/')>0,dirpath:json.path,label:json.label,listview:listview,maychangecoverart:!!isAdmin,coverarturl:encodeURIComponent(JSON.stringify({'directory':json.path})),directoryname:encodeURIComponent(json.path),foldercount:json.foldercount,showfoldercount:json.foldercount>0,filescount:json.filescount,showfilescount:json.filescount>0,filescountestimate:json.filescountestimate,};return Mustache.render(template,template_data);},_renderPlaylist:function(e){var template=templateLoader.cached('mediabrowser-playlist');var template_data={playlistid:e['plid'],isowner:e.owner,candelete:e.owner||isAdmin,playlistlabel:e['title'],username:e['username'],age:time2text(e['age']),username_color:userNameToColor(e.username),publicchecked:e['public']?'checked="checked"':'',publiclabelclass:e['public']?'label-success':'label-default',};return Mustache.render(template,template_data);},_renderCompactDirectory:function(json){var template=templateLoader.cached('mediabrowser-compact');var template_data={filepath:json.urlpath,filter:json.label,filterUPPER:json.label.toUpperCase(),};return Mustache.render(template,template_data);},addThisTrackToPlaylist:function(){"use strict" -playlistManager.addSong($(this).attr("path"),$(this).attr("title"),$(this).attr("starttime"),$(this).attr("duration"));$(this).blur();return false;},_addAllToPlaylist:function($source,plid){"use strict";$source.find('li .musicfile').each(function(){playlistManager.addSong($(this).attr("path"),$(this).attr("title"),$(this).attr("starttime"),$(this).attr("duration"),plid);});},albumArtLoader:function(cssSelector){"use strict";var winheight=$(window).height();var scrolled_down=$(cssSelector).scrollTop();var preload_threshold=50;$(cssSelector).find('.list-dir-albumart.unloaded').each(function(idx){var img_pos=$(this).position().top;var above_screen=img_poswinheight+scrolled_down+preload_threshold;if(!above_screen&&!below_screen){$(this).find('img').attr('src','api/fetchalbumart/?data='+$(this).attr('search-data'));$(this).removeClass('unloaded');}});$(cssSelector).find('.meta-info.unloaded').each(function(idx){var track_pos=$(this).parent().position().top;var above_screen=track_poswinheight+scrolled_down+preload_threshold;if(!above_screen&&!below_screen){var self=this;var path_url_enc=$(this).attr('path');var success=function(data){$(self).show();var metainfo=$.parseJSON(data);if(metainfo.artist.length>0&&metainfo.title.length>0){$(self).find('.meta-info-artist').text(metainfo.artist);$(self).find('.meta-info-title').text(metainfo.title);if(metainfo.track.length>0){if(metainfo.track.length<2){metainfo.track='0'+metainfo.track;} +MediaBrowser.static={_renderList:function(l,listview){"use strict";var self=this;var html="";$.each(l,function(i,e){switch(e.type){case'dir':html+=MediaBrowser.static._renderDirectory(e,listview);break;case'file':html+=MediaBrowser.static._renderFile(e);break;case'compact':html+=MediaBrowser.static._renderCompactDirectory(e);break;case'playlist':html+=MediaBrowser.static._renderPlaylist(e);break;default:window.console.log('cannot render unknown type '+e.type);}});return html;},_renderMessage:function(msg){var template=templateLoader.cached('mediabrowser-message');return Mustache.render(template,{message:msg});},_renderFile:function(json){var template=templateLoader.cached('mediabrowser-file');var template_data={fileurl:json.urlpath,fullpath:json.path,label:json.label,starttime:json.starttime,duration:json.duration,metainfo:json.metainfo};return Mustache.render(template,template_data);},_renderDirectory:function(json,listview){var template=templateLoader.cached('mediabrowser-directory');var template_data={isrootdir:json.path&&!json.path.indexOf('/')>0,dirpath:json.path,label:json.label,listview:listview,maychangecoverart:!!isAdmin,coverarturl:encodeURIComponent(JSON.stringify({'directory':json.path})),directoryname:encodeURIComponent(json.path),foldercount:json.foldercount,showfoldercount:json.foldercount>0,filescount:json.filescount,showfilescount:json.filescount>0,filescountestimate:json.filescountestimate,};return Mustache.render(template,template_data);},_renderPlaylist:function(e){var template=templateLoader.cached('mediabrowser-playlist');var template_data={playlistid:e['plid'],isowner:e.owner,candelete:e.owner||isAdmin,playlistlabel:e['title'],username:e['username'],age:time2text(e['age']),username_color:userNameToColor(e.username),publicchecked:e['public']?'checked="checked"':'',publiclabelclass:e['public']?'label-success':'label-default',};return Mustache.render(template,template_data);},_renderCompactDirectory:function(json){var template=templateLoader.cached('mediabrowser-compact');var template_data={filepath:json.urlpath,filter:json.label,filterUPPER:json.label.toUpperCase(),};return Mustache.render(template,template_data);},getMetaInfo:function(el){var metainfo={track:$(el).find('.meta-info-track').text(),artist:$(el).find('.meta-info-artist').text(),title:$(el).find('.meta-info-title').text(),length:$(el).attr('duration')};return metainfo;},addThisTrackToPlaylist:function(){"use strict" +playlistManager.addSong($(this).attr("path"),$(this).attr("title"),MediaBrowser.static.getMetaInfo(this),$(this).attr("starttime"),$(this).attr("duration"));$(this).blur();return false;},_addAllToPlaylist:function($source,plid){"use strict";$source.find('li .musicfile').each(function(){playlistManager.addSong($(this).attr("path"),$(this).attr("title"),MediaBrowser.static.getMetaInfo(this),$(this).attr("starttime"),$(this).attr("duration"),plid);});},albumArtLoader:function(cssSelector){"use strict";var winheight=$(window).height();var scrolled_down=$(cssSelector).scrollTop();var preload_threshold=50;$(cssSelector).find('.list-dir-albumart.unloaded').each(function(idx){var img_pos=$(this).position().top;var above_screen=img_poswinheight+scrolled_down+preload_threshold;if(!above_screen&&!below_screen){$(this).find('img').attr('src','api/fetchalbumart/?data='+$(this).attr('search-data'));$(this).removeClass('unloaded');}});$(cssSelector).find('.meta-info.unloaded').each(function(idx){var track_pos=$(this).parent().position().top;var above_screen=track_poswinheight+scrolled_down+preload_threshold;if(!above_screen&&!below_screen){var self=this;var path_url_enc=$(this).attr('path');var success=function(data){$(self).show();var metainfo=$.parseJSON(data);if(metainfo.artist.length>0&&metainfo.title.length>0){$(self).find('.meta-info-artist').text(metainfo.artist);$(self).find('.meta-info-title').text(metainfo.title);if(metainfo.track&&typeof metainfo.track==="number") +metainfo.track=metainfo.track.toString();if(metainfo.track.length>0){if(metainfo.track.length<2){metainfo.track='0'+metainfo.track;} $(self).find('.meta-info-track').text(metainfo.track);} $(self).parent().find('.simplelabel').hide();} if(metainfo.length){$(self).find('.meta-info-length').text('('+jPlayerPlaylist.prototype._formatTime(metainfo.length)+')');}} -$(this).removeClass('unloaded');api('getsonginfo',{'path':decodeURIComponent(path_url_enc),'starttime':$(self).parent().attr('starttime')},success,errorFunc('error getting song metainfo'),true);}});},};var browser=detectBrowser();if(['msie','safari'].indexOf(browser)!=-1){var encoderPreferenceOrder=['mp3','ogg'];}else{var encoderPreferenceOrder=['ogg','mp3'];} +$(this).removeClass('unloaded');api('getsonginfo',{'path':decodeURIComponent(path_url_enc)},success,errorFunc('error getting song metainfo'),true);}});$(cssSelector).find('.meta-info.preloaded').each(function(idx){$(this).removeClass('preloaded');var track=$(this).find('.meta-info-track').text();if(track.length>0){if(track.length<2){track='0'+track;} +$(this).find('.meta-info-track').text(track);} +var length=$(this).find('.meta-info-length').text();if(length.length>0){$(this).find('.meta-info-length').text('('+jPlayerPlaylist.prototype._formatTime(length)+')');}});},};var browser=detectBrowser();if(['msie','safari'].indexOf(browser)!=-1){var encoderPreferenceOrder=['mp3','ogg'];}else{var encoderPreferenceOrder=['ogg','mp3'];} var SERVER_CONFIG={};var availableEncoders=undefined;var availablejPlayerFormats=['mp3','ogg'];var availableDecoders=undefined;var transcodingEnabled=undefined;var userOptions=undefined;var isAdmin=undefined;var loggedInUserName=undefined;var REMEMBER_PLAYLIST_INTERVAL=3000;var CHECK_MUSIC_PLAYING_INTERVAL=2000;var HEARTBEAT_INTERVAL_MS=30*1000;var playlistSelector='.jp-playlist';var executeAfterConfigLoaded=[] function api(){"use strict";var action=arguments[0];var has_data=!(typeof arguments[1]==='function');var data={};if(has_data){data=arguments[1];} var successfunc=arguments[has_data?2:1];var errorfunc=arguments[has_data?3:2];var completefunc=arguments[has_data?4:3];if(!successfunc)successfunc=function(){};if(!completefunc)completefunc=function(){};var successFuncWrapper=function(successFunc){return function handler(json){var result=$.parseJSON(json);if(result.flash){successNotify(result.flash);} @@ -1244,11 +1246,11 @@ function ord(c) function showPlaylists(sortby,filterby){"use strict";var success=function(data){var addressAndPort=getAddrPort();var value_before=$('.playlist-filter-input').val();new MediaBrowser('.search-results',data,'Playlist browser',false,{showPlaylistPanel:true});$('.playlist-filter-input').val(value_before);};var error=errorFunc('error loading external playlists');busy('.search-results').hide().fadeIn('fast');api('showplaylists',{'sortby':sortby,'filterby':filterby},success,error,function(){busy('.search-results').fadeOut('fast')});} function changePlaylist(plid,attrname,value){window.console.log(plid);window.console.log(attrname);window.console.log(value);busy('#playlist-panel').hide().fadeIn('fast');api('changeplaylist',{'plid':plid,'attribute':attrname,'value':value},function(){showPlaylists();},errorFunc('error changing playlist attribute'),function(){busy('#playlist-panel').fadeOut('fast')});} function confirmDeletePlaylist(id,title){$('#deletePlaylistConfirmButton').off();$('#deletePlaylistConfirmButton').on('click',function(){busy('#playlist-panel').hide().fadeIn('fast');api('deleteplaylist',{'playlistid':id},false,errorFunc('error deleting playlist'),function(){busy('#playlist-panel').fadeOut('fast')});$('#dialog').fadeOut('fast');showPlaylists();});$('#deleteplaylistmodalLabel').html(Mustache.render('Really delete Playlist "{{title}}"',{title:title}));$('#deleteplaylistmodal').modal('show');} -function loadPlaylist(playlistid,playlistlabel){var success=function(data){var tracklist=data;var pl=playlistManager.newPlaylist([],playlistlabel);var animate=false;for(var i=0;i 0){ if(metainfo.track.length < 2){ metainfo.track = '0' + metainfo.track; @@ -412,8 +425,7 @@ MediaBrowser.static = { api( 'getsonginfo', { - 'path': decodeURIComponent(path_url_enc), - 'starttime': $(self).parent().attr('starttime') + 'path': decodeURIComponent(path_url_enc) }, success, errorFunc('error getting song metainfo'), @@ -422,6 +434,27 @@ MediaBrowser.static = { } } ); + $(cssSelector).find('.meta-info.preloaded').each( + function(idx) { + $(this).removeClass('preloaded'); + /* FIXME: Modified copy-paste of the success function above. */ + var track = $(this).find('.meta-info-track').text(); + if(track.length > 0){ + if(track.length < 2){ + track = '0' + track; + } + $(this).find('.meta-info-track').text( + track + ); + } + var length = $(this).find('.meta-info-length').text(); + if(length.length > 0) { + $(this).find('.meta-info-length').text( + '('+jPlayerPlaylist.prototype._formatTime(length)+')' + ); + } + } + ); }, } diff --git a/res/js/playlistmanager.js b/res/js/playlistmanager.js index 1ebafaae..8365c6c6 100644 --- a/res/js/playlistmanager.js +++ b/res/js/playlistmanager.js @@ -718,7 +718,7 @@ PlaylistManager.prototype = { } return track; }, - addSong : function(path, title, starttime, duration, plid, animate){ + addSong : function(path, title, metainfo, starttime, duration, plid, animate){ "use strict"; var self = this; if(typeof animate === 'undefined'){ @@ -749,37 +749,26 @@ PlaylistManager.prototype = { playlist.jplayerplaylist.select(0); } } - var success = function(data){ - var metainfo = $.parseJSON(data); - var any_info_received = false; - if (metainfo.length) { - track.duration = metainfo.length; - any_info_received = true; - } - // only show id tags if at least artist and title are known - if (metainfo.title.length > 0 && metainfo.artist.length > 0) { - track.title = metainfo.artist+' - '+metainfo.title; - if(metainfo.track.length > 0){ - track.title = metainfo.track + ' ' + track.title; - if(metainfo.track.length < 2){ - track.title = '0' + track.title; - } + var any_info_received = false; + if (metainfo.length) { + track.duration = metainfo.length; + any_info_received = true; + } + // only show id tags if at least artist and title are known + if (metainfo.title.length > 0 && metainfo.artist.length > 0) { + track.title = metainfo.artist+' - '+metainfo.title; + if(metainfo.track.length > 0){ + track.title = metainfo.track + ' ' + track.title; + if(metainfo.track.length < 2){ + track.title = '0' + track.title; } - any_info_received = true; - } - if(any_info_received){ - //only rerender playlist if it would visually change - self.getEditingPlaylist().jplayerplaylist._refresh(true); } + any_info_received = true; + } + if(any_info_received){ + //only rerender playlist if it would visually change + self.getEditingPlaylist().jplayerplaylist._refresh(true); } - // WORKAROUND: delay the meta-data fetching, so that a request - // for the actual audio data comes through frist - window.setTimeout( - function(){ - api('getsonginfo', {'path': decodeURIComponent(path), 'starttime': starttime}, success, errorFunc('error getting song metainfo'), true); - }, - 1000 - ); }, clearPlaylist : function(){ "use strict"; diff --git a/res/templates/mediabrowser-file.html b/res/templates/mediabrowser-file.html index f36a32a0..bbcc5daf 100644 --- a/res/templates/mediabrowser-file.html +++ b/res/templates/mediabrowser-file.html @@ -1,13 +1,15 @@
  • + {{^metainfo}} {{label}} -