package musicplayer import java.lang.Runtime import listfolder.ListFolder import find.Find /** * A music player. * Open a folder of music. Right-click a file for the "Play" feature. * Right-click any folder for "Play Folder" and "Random Play". * Both of these will play the entire collection of music, until "Stop" feature is fired. * * You should set MusicPlayer.musicFolder before using MusicPlayerNoneFeatureSet.randomPlay * * WARNING * ------- * If you reload the project scripts, then the "old" state won't be available. * Please STOP any playlist before reloading, otherwise you won't be able to * stop it without restarting the application (and killing the current "play" command manually). * */ class MusicPlayer() : SimpleComponent() { class val AUDIO_FILE = "AudioFile" // ==== State ==== class var musicFolder = homeFolder() class var musicFilter = $( grep '\(mp3\|flac\|wav\|ogg\)$' ) class var playing = stringProperty( "Playing Song", "" ) class var playingPath = stringProperty( "Path", "" ) class var insidePlaylist = false class var stopping = false class var paused = false class var playProcess : Process = null class val playingLabel = TextFeature( MusicPlayer.playing, MusicPlayer.playingPath ) // ==== Register ==== override meth register() { registerMajorMimeTypeContextTypes( "audio", AUDIO_FILE ) registerRunnerPrefix( "m ", "Find Music", ::prefixAction ) // ---- None --- addFeatures().apply { val randomPlayAll = Action( "Random Play", ::randomPlay.curry( MusicPlayer.musicFolder ) ) .icon( "random" ) val openMusicFolder = Action( "Open Music Folder", MusicPlayer::openMusicFolder ) .icon( "music" ) val stop = Action( "Stop", MusicPlayer::stopPlaying ) .icon( "stop" ) val skip = Action( "Skip", MusicPlayer::skip ) .icon( "skip" ) val pause = Action( "Pause", MusicPlayer::togglePause ) .icon( "pause" ) menus ( MenuFeature( "Music", openMusicFolder, randomPlayAll, stop, pause, skip ) ) toolbar( separator, openMusicFolder, randomPlayAll, stop, pause, skip, separator ) statusBar( playingLabel ) } // ---- AudioFile --- addFeatures( AUDIO_FILE ).apply { val playFile = contextAction( "Play File", ::playFileAction ) val autoTag = contextAction( "Auto-Tag", ::autoTagFileAction ) contextMenu( MenuFeature( "Music", playFile, autoTag ) ) } // ---- Folder --- addFeatures( Context.FOLDER ).apply { val playFolder = contextAction( "Play Folder", ::playFolderAction ) .icon( "play" ) val randomPlayFolder = contextAction( "Random Play", ::randomPlayAction ) .icon( "random" ) val autoTagFolder = contextAction( "Auto-Tag Folder", ::autoTagFolderAction ) menus( MenuFeature( "Music", playFolder, randomPlayFolder, autoTagFolder ) ) contextMenu( MenuFeature( "Music", playFolder, randomPlayFolder, autoTagFolder ) ) } } override meth stop() { stopPlaying() } // ==== Actions ==== func prefixAction( remainder : String ) : bool { Find.find( musicFolder, remainder ) return true } func playFileAction( context : Context ) { playFile( context.value as File ) } func playFolderAction( context : Context ) { playFolder( context.value as File ) } func randomPlayAction( context : Context ) { randomPlay( context.value as File ) } func openMusicFolder() { openContext( Context( Context.FOLDER, musicFolder ) ) } func autoTagFileAction( context : Context ) { autoTagFile( context.value as File ) } func autoTagFolderAction( context : Context ) { newThread( "AutoTagFolder", ::autoTagFolder.curry( context.value as File ) ) } // ==== Auto-Tag API ==== func autoTagFile( file : File ) { val ext = file.extension().toLowerCase() val albumDir = file.parentFile val fileName = file.nameWithoutExtension() // Extact the track and title from a filename. e.g. // (01) First (1) First 01 First 1 First val pattern = Pattern.compile( "\\(?0*([0-9]*)\\)?\\w*(.*)" ) val matcher = pattern.matcher( fileName ) matcher.find() val track = matcher.group(1) val rawTitle = matcher.group(2) val title = rawTitle .replaceAll( "\\{.+\\}", "" ) // {...} post processing info .replaceAll( "\\[.+\\]", "" ) // [...] Song writers .replaceAll( "\\([0-9]*\\)", "" ) // (nnnn) Year .replaceAll( "\\s+", " " ) // Duplicate white space .trim() // White space at EOL val album = albumDir.name val artist = albumDir.parentFile.name val command = if (ext == "mp3") { $( id3 -T '${track}'-t '${title} -A '${album}' -a '${artist}' '$file' ) } else if (ext == "ogg" ) { $( vorbiscomment -w -t TRACK='${track}' -t TITLE='${title}' -t ARTIST='${artist}' -t ALBUM='${album}' '$file' ) } else if (ext == "flac" ) { $( metaflac --remove-all-tags --set-tag=TRACK='${track}' --set-tag=TITLE='${title}' --set-tag=ARTIST='${artist}' --set-tag=ALBUM='${album}' '$file' ) } else { printError( "Auto-tag doesn't support files type : '$ext'") $( exit 1 ) } val result = command.run() if (result.exitStatus == 0) { println( "OK : $command" ) } else { println( "FAILED : $command" ) } } func autoTagFolder( folder : File ) { val extensions = listOf( "ogg", "flac", "mp3" ) for ( file in folder.files() ) { if ( extensions.contains( file.extension().toLowerCase() ) ) { autoTagFile( file ) } } for ( sub in folder.folders() ) { autoTagFolder( sub ) } } // ==== Music Player API ==== func playFile( file : File ) { _stopIfPlaying() _playFile(file) } func playFolder( folder : File ) { // println( "Playing folder : $folder" ) _startPlaylist( folder, musicFilter | $( sort ) ) } func randomPlay( folder : File ) { // println( "Random play of $folder" ) _startPlaylist( folder, musicFilter | $( sort -R ) ) } func stopPlaying() { _stop( true ) } /** * Stops the current song, but lets the playlist continue */ func skip() { _stop( false ) } func togglePause() { val process = playProcess // println( "Toggle pause of : $process" ) if (process == null) { printInfo( "Nothing is playing - pause does nothing" ) return } if (paused) { ProcessBuilder("kill", "--signal", "CONT", "${process.pid()}").start() } else { ProcessBuilder("kill", "--signal", "STOP", "${process.pid()}").start() } paused = !paused } // ==== Private ==== func _stopIfPlaying() : Boolean { // println( "Inside playlist : $insidePlaylist" ) if ( insidePlaylist ) { printInfo( "Cannot start playing before the old playlist has stopped" ) stopPlaying() return true } val process = playProcess if (process != null && process.isAlive()) { stopPlaying() } return false } func _startPlaylist( folder : File, filter : CommandBase ) { if (_stopIfPlaying()) { // ( "Trying again in 1 second" ) delay( 1000, ::_startPlaylist.curry(folder).curry( filter ) ) } else { newThread( "MusicPlayer", ::_playThreaded.curry( folder ).curry( filter ) ) } } func _playFile( file : File ) : Process { playing.value = file.nameWithoutExtension().replaceFirst( "^\\(.+\\) *", "" ).replaceFirst( "\\[.*\\]", "" ) playingPath.value = file.path printInfo( "Playing file ${file.name}" ) // We can't use Command, because that returns the process of the `sh -c` process. Grr. val process = ProcessBuilder( "play", "-q", file.path ).start() playProcess = process return process } func _stop( cancelPlaylist : Boolean ) { playing.value = "" playingPath.value = "" if (insidePlaylist && cancelPlaylist ) { stopping = true } val process = playProcess if (process != null) { // printInfo( "Stopping process $process" ) playProcess.destroy() playProcess = null paused = false } } func _playThreaded( folder : File, filter : CommandBase ) { insidePlaylist = true try { val command = $( find '$folder' ) | filter val output = command.eval() val filePaths = output.split("\n") for (filePath in filePaths) { if (stopping) { // println( "Stopping, so break" ) break } try { _playFile( File(filePath) ).waitFor() } catch ( e : Exception ) { printError( "Music Player error : $e " ) } finally { playProcess = null } } printInfo( "Finished the playlist" ) } finally { insidePlaylist = false stopping = false } } }