diff --git a/audiotranscode/__init__.py b/audiotranscode/__init__.py index 6962703b..7a17d462 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,30 @@ def __init__(self, filetype, command): self.filetype = filetype self.mimetype = MimeTypes[filetype] - def decode(self, filepath, starttime=0): + def decode(self, filepath, starttime=0, duration=None): + # Fetch audio filepath from cue sheets + ext = os.path.splitext(filepath)[1] + if ext == '.cue': + from cherrymusicserver.cuesheet import Cuesheet + cue = Cuesheet(filepath) + for cdtext in cue.info[0].cdtext: + if cdtext.type == 'FILE': + filepath = os.path.join(os.path.dirname(filepath), cdtext.value[0]) + break cmd = self.command[:] - 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) + # 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, stdout=subprocess.PIPE, stderr=Transcoder.devnull @@ -107,6 +125,7 @@ class AudioTranscode: Decoder('aac' , ['faad', '-w', 'INPUT']), Decoder('m4a' , ['faad', '-w', 'INPUT']), Decoder('wav' , ['cat', 'INPUT']), + Decoder('cue' , ['ffmpeg', '-ss', 'STARTTIME', '-t', 'DURATION', '-i', 'INPUT', '-f', 'wav', '-']), ] def __init__(self,debug=False): @@ -125,7 +144,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, duration=None): if not os.path.exists(filepath): filepath = os.path.abspath(filepath) raise DecodeError('File not Found! Cannot decode "file" %s'%filepath) @@ -139,7 +158,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, duration=duration) def _encode(self, audio_format, decoder_process, bitrate=None,encoder=None): if not audio_format in self.availableEncoderFormats(): @@ -166,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): + encoder=None, decoder=None, starttime=0, duration=None): decoder_process = None encoder_process = None try: - decoder_process = self._decode(filepath, decoder, starttime=starttime) + 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 a83a5a25..317d9026 100644 --- a/cherrymusicserver/cherrymodel.py +++ b/cherrymusicserver/cherrymodel.py @@ -163,10 +163,40 @@ def listdir(self, dirpath, filterstr=''): musicentry.count_subfolders_and_files() return musicentries + @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: + 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)s'"), {'filepath': filepath}) + return + for track_n, track in enumerate(cue.tracks, 1): + starttime = track.get_start_time() + # 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, metainfo = metainfo)) + @classmethod def addMusicEntry(cls, fullpath, list): if os.path.isfile(fullpath): - if CherryModel.isplayable(fullpath): + if CherryModel.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 +343,10 @@ def isplayable(cls, filename): is_empty_file = os.path.getsize(CherryModel.abspath(filename)) == 0 return is_supported_ext and not is_empty_file + @staticmethod + 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 +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): + 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 @@ -336,6 +370,10 @@ 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 + # 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: @@ -381,6 +419,9 @@ def to_dict(self): return {'type': 'file', 'urlpath': urlpath, '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/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..d9ede2b2 100644 --- a/cherrymusicserver/httphandler.py +++ b/cherrymusicserver/httphandler.py @@ -252,14 +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)) + starttime = float(params.pop('starttime', 0)) + if 'duration' in params: + duration = float(params.pop('duration')) + else: + 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) + bitrate=bitrate, starttime=starttime, duration=duration) except audiotranscode.TranscodeError as e: raise cherrypy.HTTPError(404, e.value) trans.exposed = True diff --git a/cherrymusicserver/metainfo.py b/cherrymusicserver/metainfo.py index 238152fe..662b6a0d 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,30 @@ def dict(self): 'length': self.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 = None + log.warn(_("Couldn't get length of '%s', setting 0"), audiofilepath) + else: + audiolength = tag.duration - track.get_start_time() + 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/cherrymusicserver/test/test_httphandler.py b/cherrymusicserver/test/test_httphandler.py index 420ae4b5..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, 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 94d54c1a..bd7d8bf7 100644 --- a/res/dist/cherrymusic.dist.js +++ b/res/dist/cherrymusic.dist.js @@ -1100,7 +1100,7 @@ $(document).on('click.bs.alert.data-api',dismiss,Alert.prototype.close)}(window. 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;}} @@ -1138,20 +1138,21 @@ 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,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)},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=[] @@ -1189,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,};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'],encodedplaylistlabel:encodeURI(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,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)},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'];} +if(metainfo.length){$(self).parent().attr('duration',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);}});$(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);} @@ -1242,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; @@ -402,6 +416,7 @@ MediaBrowser.static = { } // show length anyway, if it was detemined. if(metainfo.length){ + $(self).parent().attr('duration', metainfo.length); $(self).find('.meta-info-length').text( '('+jPlayerPlaylist.prototype._formatTime(metainfo.length) + ')' ); @@ -410,7 +425,9 @@ MediaBrowser.static = { $(this).removeClass('unloaded'); api( 'getsonginfo', - {'path': decodeURIComponent(path_url_enc)}, + { + 'path': decodeURIComponent(path_url_enc) + }, success, errorFunc('error getting song metainfo'), true @@ -418,6 +435,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 c5f5a025..8365c6c6 100644 --- a/res/js/playlistmanager.js +++ b/res/js/playlistmanager.js @@ -104,6 +104,7 @@ ManagedPlaylist.prototype = { var elem = this.jplayerplaylist.playlist[i]; var track = { title : elem.title, + starttime: elem.starttime, duration : elem.duration, url: elem.url, } @@ -667,10 +668,14 @@ PlaylistManager.prototype = { var self = this; var path = track.url; var title = track.title; + var starttime = track.starttime; + var duration = track.duration; var ext = getFileTypeByExt(path); var track = { title: title, wasPlayed : 0, + starttime: starttime, + duration: duration } var forced_bitrate = userOptions.media.force_transcode_to_bitrate; var formats = []; @@ -697,6 +702,10 @@ PlaylistManager.prototype = { formats.push(availablejPlayerFormats[i]); var transurl = SERVER_CONFIG.transcode_path + availablejPlayerFormats[i] + '/' + path; transurl += '?bitrate=' + forced_bitrate; + if(track.starttime) + transurl += '&starttime=' + track.starttime; + if(track.duration) + transurl += '&duration=' + track.duration; track[ext2jPlayerFormat(availablejPlayerFormats[i])] = transurl; window.console.log('added live transcoding '+ext+' --> '+availablejPlayerFormats[i]+' @ '+transurl); } @@ -709,7 +718,7 @@ PlaylistManager.prototype = { } return track; }, - addSong : function(path, title, plid, animate){ + addSong : function(path, title, metainfo, starttime, duration, plid, animate){ "use strict"; var self = this; if(typeof animate === 'undefined'){ @@ -718,6 +727,8 @@ PlaylistManager.prototype = { var track = { title: title, url: path, + starttime: starttime, + duration: duration, wasPlayed : 0, } var playlist; @@ -738,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)}, 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 e9e79483..bbcc5daf 100644 --- a/res/templates/mediabrowser-file.html +++ b/res/templates/mediabrowser-file.html @@ -1,16 +1,18 @@
  • - + + {{^metainfo}} {{label}} - -
  • \ No newline at end of file +