Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Port iTunes appscript to applescript. #178 #181

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions Rakefile
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -39,6 +39,15 @@ task :start do
Kernel.exec "bundle exec foreman start" Kernel.exec "bundle exec foreman start"
end end


desc "Warm up song cache"
task :warm_cache do
# Cache all Song data.
songs = `osascript -e 'tell application "iTunes" to get persistent ID of every track'`.chomp.split(", ")
songs.each do |id|
Play::Song.find(id)
end
end

namespace :redis do namespace :redis do
desc "Wipe all data in redis" desc "Wipe all data in redis"
task :reset do task :reset do
Expand Down
23 changes: 15 additions & 8 deletions app/api/system.rb
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -4,18 +4,25 @@ class App < Sinatra::Base


get "/images/art/:id.png" do get "/images/art/:id.png" do
content_type 'image/png' content_type 'image/png'
cached_artwork = "/tmp/play-artwork/#{params[:id]}.png"


song = Song.find(params[:id]) if File.exists? cached_artwork
art = song.album_art_data if song

if art
response['Cache-Control'] = 'public, max-age=2500000' response['Cache-Control'] = 'public, max-age=2500000'
etag params[:id] etag params[:id]
art send_file cached_artwork, :disposition => 'inline'
else else
dir = File.dirname(File.expand_path(__FILE__)) song = Song.find(params[:id])
send_file "#{dir}/../frontend/public/images/art-placeholder.png", art = song.album_art_data if song
:disposition => 'inline'
if art
FileUtils.mkdir_p("/tmp/play-artwork")
File.write(cached_artwork, art)
art
else
dir = File.dirname(File.expand_path(__FILE__))
send_file "#{dir}/../frontend/public/images/art-placeholder.png",
:disposition => 'inline'
end
end end
end end


Expand Down
13 changes: 6 additions & 7 deletions app/models/album.rb
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -22,20 +22,19 @@ def initialize(name,artist)
# #
# Returns an Array of Songs. # Returns an Array of Songs.
def self.songs_by_name(name) def self.songs_by_name(name)
Player.library.file_tracks[Appscript.its.album.eq(name)].get.map do |record| songs = `osascript -e 'tell application "iTunes" to get persistent ID of every track whose album is \"#{name}\"'`.chomp.split(", ")
Song.new(record.persistent_ID.get) if songs.empty?
nil
else
songs.map { |id| Song.find(id) }
end end
end end


# The songs attached to this album. # The songs attached to this album.
# #
# Returns an Array of Songs. # Returns an Array of Songs.
def songs def songs
Player.library.file_tracks[ Album.songs_by_name(self.name)
Appscript.its.album.eq(name).and(Appscript.its.artist.eq(artist))
].get.map do |record|
Song.new(record.persistent_ID.get)
end
end end


# Zips up an album and stashes in it a temporary directory. # Zips up an album and stashes in it a temporary directory.
Expand Down
9 changes: 4 additions & 5 deletions app/models/artist.rb
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -17,12 +17,11 @@ def initialize(name)
# #
# Returns an Array of Songs. # Returns an Array of Songs.
def songs def songs
if name songs = `osascript -e 'tell application "iTunes" to get persistent ID of every track whose artist is \"#{self.name}\"'`.chomp.split(", ")
Player.app.tracks[Appscript.its.artist.contains(name)].get.map do |record| if songs.empty?
Song.initialize_from_record(record) nil
end
else else
[] songs.map { |id| Song.find(id) }
end end
end end


Expand Down
76 changes: 45 additions & 31 deletions app/models/player.rb
Original file line number Original file line Diff line number Diff line change
@@ -1,50 +1,47 @@
module Play module Play
class Player class Player


# The application we're using. iTunes, dummy.
#
# Returns an Appscript instance of the music app.
def self.app
Appscript.app('iTunes')
end

# All songs in the library.
def self.library
app.playlists['Library'].get
end

# Play the music. # Play the music.
def self.play def self.play
app.play `osascript -e 'tell application "iTunes" to play'`
end end


# Pause the music. # Pause the music.
def self.pause def self.pause
app.pause `osascript -e 'tell application "iTunes" to pause'`
end

# Get current state.
#
# Returns symbol.
def self.state
`osascript -e 'tell application "iTunes" to get player state'`.chomp.to_sym
end end


# Is there music currently playing? # Is there music currently playing?
def self.paused? def self.paused?
state = app.player_state.get
state == :paused state == :paused
end end


# Maybe today is the day the music stopped. # Maybe today is the day the music stopped.
def self.stop def self.stop
app.stop `osascript -e 'tell application "iTunes" to stop'`
end end


# Play the next song. # Play the next song.
# #
# Returns the new song. # Returns the new song.
def self.play_next def self.play_next
app.next_track `osascript -e 'tell application "iTunes" to play next track'`
now_playing now_playing
end end


# Play the previous song. # Play the previous song.
#
# Returns the new song.
def self.play_previous def self.play_previous
app.previous_track `osascript -e 'tell application "iTunes" to play previous track'`
now_playing
end end


# Get the current numeric volume. # Get the current numeric volume.
Expand All @@ -69,7 +66,7 @@ def self.system_volume=(setting)
# #
# Returns an Integer from 0-100. # Returns an Integer from 0-100.
def self.app_volume def self.app_volume
app.sound_volume.get `osascript -e 'tell application "iTunes" to get sound volume'`.chomp.to_i
end end


# Set the app volume. # Set the app volume.
Expand All @@ -79,7 +76,7 @@ def self.app_volume
# #
# Returns the current volume setting. # Returns the current volume setting.
def self.app_volume=(setting) def self.app_volume=(setting)
app.sound_volume.set(setting) `osascript -e 'tell application "iTunes" to set sound volume to #{setting}'`
setting setting
end end


Expand All @@ -94,13 +91,22 @@ def self.say(message)
self.app_volume = previous self.app_volume = previous
end end


# Get persistent id of current track.
#
# Returns string.
def self.current_track
`osascript -e 'tell application "iTunes" to get persistent ID of current track'`.chomp
end

# Currently-playing song. # Currently-playing song.
# #
# Returns a Song. # Returns a Song.
def self.now_playing def self.now_playing
Song.new(app.current_track.persistent_ID.get) if state == :playing
rescue Appscript::CommandError Song.find(current_track)
nil else
nil
end
end end


# Search all songs for a keyword. # Search all songs for a keyword.
Expand All @@ -115,20 +121,28 @@ def self.now_playing
# Returns an Array of matching Songs. # Returns an Array of matching Songs.
def self.search(keyword) def self.search(keyword)
# Exact Artist match. # Exact Artist match.
songs = library.tracks[Appscript.its.artist.eq(keyword)].get songs = `osascript -e 'tell application "iTunes" to get persistent ID of every track whose artist is \"#{keyword}\"' 2>&1`.chomp.split(", ")
return songs.map{|record| Song.new(record.persistent_ID.get)} if songs.size != 0 if $? == 0 && !songs.empty?
return songs.map { |id| Song.find(id) }
end


# Exact Album match. # Exact Album match.
songs = library.tracks[Appscript.its.album.eq(keyword)].get songs = `osascript -e 'tell application "iTunes" to get persistent ID of every track whose album is \"#{keyword}\"' 2>&1`.chomp.split(", ")
return songs.map{|record| Song.new(record.persistent_ID.get)} if songs.size != 0 if $? == 0 && !songs.empty?
return songs.map { |id| Song.find(id) }
end


# Exact Song match. # Exact Song match.
songs = library.tracks[Appscript.its.name.eq(keyword)].get songs = `osascript -e 'tell application "iTunes" to get persistent ID of every track whose name is \"#{keyword}\"' 2>&1`.chomp.split(", ")
return songs.map{|record| Song.new(record.persistent_ID.get)} if songs.size != 0 if $? == 0 && !songs.empty?
return songs.map { |id| Song.find(id) }
end


# Fuzzy Song match. # Fuzzy Song match.
songs = library.tracks[Appscript.its.name.contains(keyword)].get songs = `osascript -e 'tell application "iTunes" to get persistent ID of every track whose name contains \"#{keyword}\"' 2>&1`.chomp.split(", ")
songs.map{|record| Song.new(record.persistent_ID.get)} if $? == 0 && !songs.empty?
return songs.map { |id| Song.find(id) }
end
end end


end end
Expand Down
52 changes: 25 additions & 27 deletions app/models/queue.rb
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -11,15 +11,11 @@ def self.name
'iTunes DJ' 'iTunes DJ'
end end


# The Playlist object that the Queue resides in.
#
# Returns an Appscript::Reference to the Playlist.
def self.playlist
Player.app.playlists[name].get
end

# Get the queue start offset for the iTunes DJ playlist. # Get the queue start offset for the iTunes DJ playlist.
# #
# Bug: When player is stopped with no song, returns 0 resulting
# in queue including any song history from iTunes DJ.
#
# iTunes DJ keeps the current song in the playlist and # iTunes DJ keeps the current song in the playlist and
# x number of songs that have played. This method returns # x number of songs that have played. This method returns
# the current song index in the playlist. Using this we # the current song index in the playlist. Using this we
Expand All @@ -33,7 +29,11 @@ def self.playlist
# #
# Returns Integer offset to queued songs. # Returns Integer offset to queued songs.
def self.playlist_offset def self.playlist_offset
Player.app.current_track.index.get if [:playing, :paused].include?(Player.state)
`osascript -e 'tell application "iTunes" to get index of current track'`.chomp.to_i
else
0
end
end end


# Public: Adds a song to the Queue. # Public: Adds a song to the Queue.
Expand All @@ -42,7 +42,7 @@ def self.playlist_offset
# #
# Returns a Boolean of whether the song was added. # Returns a Boolean of whether the song was added.
def self.add_song(song) def self.add_song(song)
Player.app.add(song.record.location.get, :to => playlist.get) `osascript -e 'tell application "iTunes" to add add (get location of first track whose persistent ID is \"#{song.id}\") to playlist \"#{name}\"'`
end end


# Public: Removes a song from the Queue. # Public: Removes a song from the Queue.
Expand All @@ -51,50 +51,48 @@ def self.add_song(song)
# #
# Returns a Boolean of whether the song was removed maybe. # Returns a Boolean of whether the song was removed maybe.
def self.remove_song(song) def self.remove_song(song)
Play::Queue.playlist.tracks[ `osascript -e 'tell application "iTunes" to delete (first track of playlist \"#{name}\" whose persistent ID is \"#{song.id}\")'`
Appscript.its.persistent_ID.eq(song.id)
].first.delete
end end


# Clear the queue. Shit's pretty destructive. # Clear the queue. Shit's pretty destructive.
# #
# Returns who the fuck knows. # Returns nil.
def self.clear def self.clear
Play::Queue.playlist.tracks.get.each { |record| record.delete } `osascript -e 'tell application "iTunes" to delete (every track of playlist \"#{name}\")'`
nil
end end


# Ensure that we're currently playing on the Play playlist. Don't let anyone # Ensure that we're currently playing on the Play playlist. Don't let anyone
# else use iTunes, because fuck 'em. # else use iTunes, because fuck 'em.
# #
# Returns nil. # Returns nil.
def self.ensure_playlist def self.ensure_playlist
if Play::Player.app.current_playlist.get.name.get != name current_playlist = `osascript -e 'tell application "iTunes to get name of current playlist'`.chomp
Play::Player.app.playlists[name].get.play if current_playlist != name
`osascript -e 'tell application "iTunes" to play playlist \"#{name}\"'`
end end
rescue Exception => e nil
# just in case!
end end


# The songs currently in the Queue. # The songs currently in the Queue.
# #
# Returns an Array of Songs. # Returns an Array of Songs.
def self.songs def self.songs
songs = playlist.tracks.get.map do |record| songs = `osascript -e 'tell application "iTunes" to get persistent ID of every track of playlist \"#{name}\"'`.chomp.split(", ")
Song.find(record.persistent_ID.get) if songs.empty?
nil
else
songs.map! { |id| Song.find(id) }
songs.slice(playlist_offset, songs.length - playlist_offset)
end end
songs.slice(playlist_offset, songs.length - playlist_offset)
rescue Exception => e
# just in case!
nil
end end


# Is this song queued up to play? # Is this song queued up to play?
# #
# Returns a Boolean. # Returns a Boolean.
def self.queued?(song) def self.queued?(song)
Play::Queue.playlist.tracks[ queued = `osascript -e 'tell application "iTunes" to get exists (first track of playlist \"#{name}\" whose persistent ID is \"#{song.id}\")'`.chomp.to_sym
Appscript.its.persistent_ID.eq(song.id) queued == :true
].get.size != 0
end end


# Returns the context of this Queue as JSON. This contains all of the songs # Returns the context of this Queue as JSON. This contains all of the songs
Expand Down
Loading