#!/usr/bin/env ruby ########################################################################## # AmarokNowPlaying pour Facebook - Show what you're listening to on # # your Facebook profile. # # Note: In order to use the script, you must choose secrets there : # # # # Copyright (C) 2008 Jany Belluz # # # # 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 . # ########################################################################## # Main script # ########################################################################## # Version $version = '0.8' # Default configuration, you can modify this : # Configuration par défaut, vous pouvez bidouiller : # Where the AmarokNowPlaying PHP scripts are hosted: # Adresse à laquelle sont hébergés les scripts PHP AmarokNowPlaying : $remoteAppBase = 'http://similitux.elementfx.com/dev/nowplaying/api' # Default size of album cover thumbnails, in pixels, from 1 to 130: # Taille par défaut des miniatures de pochette d'album, en pixels, de 1 à 130 : $coverSize = 75 # Prefered dialog manager (if available on your system) # Gestionnaire de dialogue préféré (selon disponibilité sur votre système) # 'kdialog' (KDE) or 'zenity' (GNOME) $prefered_dialog = 'kdialog' # Debug mode: $debug_http = false # Dependencies require 'net/http' # Standard library require 'stringio' # Standard library (needed by 'multipart.rb') require 'digest/md5' # Standard library require 'uri' # Standard library # Directories $defaultDir = Dir.pwd # Working directory given by Amarok # ~/.kde/share/apps/amarok/script-data/ Dir.chdir( "../scripts/fbAmarokNowPlaying/dependencies" ) $libDir = Dir.pwd # External ruby dependencies directory # ~/.kde/share/apps/amarok/scripts/fbAmarokNowPlaying/dependencies require 'ini' # Included (in $libDir) require 'multipart' # Included (in $libDir) require 'dialog' # Included (in $libDir) require 'version' # Included (in $libDir) ########################################################################## # Notes about this script : # - The functions loadFiles() and loadConfig() must be called before any else # - Translations are stored in file $libDir/translations.ini # - Config is stored in $defaultDir/fbAmarokNowPlaying.ini ########################################################################## # Shows a message (uses dcop and Dialog) # Possible 'type' are 'status', for Amarok statusbar, 'popup' for a passive # popup in Amarok main window, and 'dialog' for a windowed dialog. def showMessage( message, title = '', type = 'popup' ) message = message.to_s title = title.to_s type = type.to_s case type when 'status' `dcop amarok playlist shortStatusMessage "#{ tr('AmarokNowPlaying for Facebook:') + ' ' + ((title + '. ') unless title.empty?).to_s + message }"` when 'popup' `dcop amarok playlist popupMessage "#{ tr('AmarokNowPlaying for Facebook:') + ' ' + ((title + '. ') unless title.empty?).to_s + message }"` when 'dialog' Dialog.message( message, title ) else `dcop amarok playlist popupMessage "#{ tr('AmarokNowPlaying for Facebook:') + ' ' + ((title + '. ') unless title.empty?).to_s + message }"` end end ########################################################################## # Shows language configuration dialogs def askForLanguageConfig() if $translationFile languages = [] $translationFile.each_section do |language| languages << language.to_s end $language = $configFile['General']['language'] = Dialog.chooser("Choose your language:", languages, 'radio', $language.to_s ).to_s.chomp() end end ########################################################################## # Shows cover configuration dialogs def askForCoverConfig() if system("convert -version") Dialog.message(tr('Do you want this script to send album covers to server? It has a little bandwidth cost.'), tr('Covers'), 'question') $configFile['General']['sendAlbumCover'] = $sendAlbumCover = ( Dialog.last_return_code == Dialog::OK_YES_CONTINUE ? true : false ) else Dialog.message tr('Cannot find the `convert` program (from ImageMagick package) on your system. You cannot use album covers upload.') $configFile['General']['sendAlbumCover'] = $sendAlbumCover = false end if $sendAlbumCover $configFile['General']['coverSize'] = $coverSize = Dialog.input( tr('Size in pixels of cover thumbnails that will be shown on Facebook:'), tr('AmarokNowPlaying for Facebook:'), $configFile['General']['coverSize'].to_s.empty? ? '75' : $configFile['General']['coverSize'].to_s ).to_i if $coverSize == 0 $configFile['General']['coverSize'] = $coverSize = 75 elsif $coverSize < 20 $configFile['General']['coverSize'] = $coverSize = 20 elsif $coverSize > 130 $configFile['General']['coverSize'] = $coverSize = 130 end end end ########################################################################## # Shows connection dialogs (uses kdialog) def askForConnect() $configFile['General']['pseudo'] = $userPseudo = Dialog.input( "#{ tr('Please enter your pseudo to connect to server:') }\n\n(#{ tr('In order to use the script, you must sign up there:') }\nhttp://apps.facebook.com/amaroknowplaying/)", tr('Connection'), ($configFile['General']['pseudo'] if ! $configFile['General']['pseudo'].to_s.empty?) ) $configFile['General']['password'] = $userPassword = Digest::MD5.hexdigest( URI.encode( Dialog.input( tr('Please enter your password to connect to server:'), tr('Connection'), '', true ) + '_Pouet!', /[^a-zA-Z0-9.-_]/ ) ).to_s() end ########################################################################## # Shows "special comment" dialogs (uses kdialog) def askForSpecialComment() $specialMessage = Dialog.input( "#{ tr('Special comment:') } #{ tr('enter your message') }", tr('Connection'), ($configFile['General']['pseudo'] if ! $configFile['General']['pseudo'].to_s.empty?) ) $specialComment = Dialog.input( tr('Please enter your password to connect to server:'), tr('Connection'), '', true ) + '_Pouet!', /[^a-zA-Z0-9.-_]/ ) ).to_s() end ########################################################################## # Loads needed INI files def loadFiles() # Loads configuration ini file Dir.chdir( $defaultDir ) begin $configFile = Ini.new( 'fbAmarokNowPlaying.ini' ) rescue Ini::Error # Configuration file is not "parsable" or does not exist, we empty it File.new( 'fbAmarokNowPlaying.ini', 'w' ) $configFile = Ini.new( 'fbAmarokNowPlaying.ini' ) end # Loads translations ini file Dir.chdir( $libDir ) begin $translationFile = Ini.new( 'translations.ini' ) rescue Ini::Error $translationFile = false end end ########################################################################## # Loads the script's configuration def loadConfig( reconfigureAll = false ) # Configures Dialog class Dialog.pre_title = tr('AmarokNowPlaying for Facebook:') Dialog.set_prefered($prefered_dialog) # Set current directory to $defaultDir, because the $configFile must # be written in it. Dir.chdir( $defaultDir ) addMenuEntry() # Loads language config $language = $configFile['General']['language'].to_s() if $language.empty? || reconfigureAll askForLanguageConfig() $configFile.write() # Re-configures Dialog class (with updated translation) Dialog.pre_title = tr('AmarokNowPlaying for Facebook:') removeMenuEntry() addMenuEntry() end # Loads album cover config $sendAlbumCover = $configFile['General']['sendAlbumCover'].to_s() $coverSize = $configFile['General']['coverSize'].to_i() if $sendAlbumCover.empty? || reconfigureAll askForCoverConfig() $configFile.write() end if $sendAlbumCover == 'true' || $sendAlbumCover == true $sendAlbumCover = true else $sendAlbumCover = false end # Loads secrets $userPseudo = $configFile['General']['pseudo'].to_s() $userPassword = $configFile['General']['password'].to_s() # Tests secrets if reconfigureAll or ( ! submitTrack( 'test' ) ) askForConnect() $configFile.write() if ! submitTrack( 'test' ) # Connection failed showMessage(tr('Connection failed, so this script will stop. If you want to retry, just restart it. Warning: In order to use the script, you must choose secrets there: http://apps.facebook.com/amaroknowplaying/'), tr('Login Error'), 'dialog') exit end end submitTrack() end ########################################################################## # Does translations def tr( string ) tr_string = if $translationFile and $translationFile[$language] and ! $translationFile[$language][string.to_s].to_s.empty? $translationFile[$language][string.to_s] else string end.to_s tr_string['"'] = '\"' if tr_string.include? '"' tr_string end ########################################################################## # Sends the current track to serveur. Returns false if and only if # secrets are wrong, else returns true, even if errors occured. def submitTrack( param = 'post' ) url = URI.parse( $remoteAppBase.to_s + ( $remoteAppBase.to_s[-1] == '/' ? 'post.php' : '/post.php') ) req = Net::HTTP::Post.new(url.path) req['User-Agent'] = "AmarokNowPlaying Script #{ $version }" if ( ENV['http_proxy'].to_s().empty?() ) newhttp = Net::HTTP.new(url.host, url.port) else proxy_url = URI.parse(ENV['http_proxy']) if proxy_url.userinfo proxy_user, proxy_pass = proxy_url.userinfo.split(/:/) newhttp = Net::HTTP.new(url.host, url.port, proxy_url.host, proxy_url.port, proxy_user, proxy_pass) else newhttp = Net::HTTP.new(url.host, url.port, proxy_url.host, proxy_url.port) end end if ( `dcop amarok player isPlaying`.to_s().chomp() == 'false' ) param = 'none' end begin case param when 'test' req.set_form_data( {:param => 'test', :pseudo => $userPseudo, :password => $userPassword} ) res = newhttp.start {|http| http.request(req) } when 'none' req.set_form_data( {:param => 'none', :pseudo => $userPseudo, :password => $userPassword} ) res = newhttp.start {|http| http.request(req) } when 'post' or 'special' albumCoverImage = nil if $sendAlbumCover # Makes a small image wich is sent to post.php with track information. coverPath = `dcop amarok player coverImage`.to_s.chomp() # Informations about image: # coverData[1] = path # coverData[2] = resolution or nil # coverData[3] = amarok album's hash-code or 'nocover' # coverData[4] = '@shadow' or nil coverData = /(.*)\/(\d+)?@?([0-9a-f]+|nocover)(@shadow)?/.match(coverPath) if coverData and coverData[3] != 'nocover' Dir.chdir( coverData[1] ) # If file (shadowed, good size) is already made, take it if Kernel.test( ?f, $coverSize.to_s + '@' + coverData[3] + '@shadow' ) albumCoverImage = Net::HTTP::FileForPost.new(coverData[1] + '/' + $coverSize.to_s + '@' + coverData[3] + '@shadow', 'image/png') # Else if we can have a shadowed thumbnail of a bad size... elsif Kernel.test( ?f, coverData[2] + '@' + coverData[3] + '@shadow' ) # ...we use `convert` to get a good-sized thumbnail from it `convert "./#{ coverData[2] + '@' + coverData[3] + '@shadow' }" -resize #{ $coverSize.to_s } "#{ $coverSize.to_s + '@' + coverData[3] + '@shadow' }"` if ( $?.exitstatus == 0 ) albumCoverImage = Net::HTTP::FileForPost.new(coverData[1] + '/' + $coverSize.to_s + '@' + coverData[3] + '@shadow', 'image/png') end # Else if (good size) is already made, take it elsif Kernel.test( ?f, $coverSize.to_s + '@' + coverData[3] ) albumCoverImage = Net::HTTP::FileForPost.new(coverData[1] + '/' + $coverSize.to_s + '@' + coverData[3], 'image/png') # Else if we can have a normal thumbnail of a bad size... elsif Kernel.test( ?f, coverData[2] + '@' + coverData[3] ) # ...we use `convert` to get a good-sized thumbnail from it `convert "./#{ coverData[2] + '@' + coverData[3] }" -resize #{ $coverSize.to_s } "#{ $coverSize.to_s + '@' + coverData[3] }"` if ( $?.exitstatus == 0 ) albumCoverImage = Net::HTTP::FileForPost.new(coverData[1] + '/' + $coverSize.to_s + '@' + coverData[3], 'image/png') end end end end if $sendAlbumCover && albumCoverImage req.set_multipart_data( {:cover => albumCoverImage}, {:param => 'post' , :pseudo => $userPseudo, :password => $userPassword, :title => `dcop amarok player title`.to_s.chomp, :artist => `dcop amarok player artist`.to_s.chomp, :album => `dcop amarok player album`.to_s.chomp, :score => `dcop amarok player score`.to_s.chomp, :rating => `dcop amarok player rating`.to_s.chomp, :counter => `dcop amarok player trackPlayCounter`.to_s.chomp, :genre => `dcop amarok player genre`.to_s.chomp, :length => `dcop amarok player totalTime`.to_s.chomp, :year => `dcop amarok player year`.to_s.chomp, :amarok_ver => `dcop amarok player version`.to_s.chomp} ) res = newhttp.start {|http| http.request(req) } else req.set_form_data( {:param => 'post', :pseudo => $userPseudo, :password => $userPassword, :title => `dcop amarok player title`.to_s.chomp, :artist => `dcop amarok player artist`.to_s.chomp, :album => `dcop amarok player album`.to_s.chomp, :score => `dcop amarok player score`.to_s.chomp, :rating => `dcop amarok player rating`.to_s.chomp, :counter => `dcop amarok player trackPlayCounter`.to_s.chomp, :genre => `dcop amarok player genre`.to_s.chomp, :length => `dcop amarok player totalTime`.to_s.chomp, :year => `dcop amarok player year`.to_s.chomp, :amarok_ver => `dcop amarok player version`.to_s.chomp} ) res = newhttp.start {|http| http.request(req) } end end case res when Net::HTTPSuccess body = res.body.to_s() showMessage(body.tr('"', "''"), 'Debug') if $debug_http if body.include? 'OK' # All is fine showMessage(tr('Track informations sent'), '', 'status') return true elsif body.include? 'QUERY' # Request is wrong showMessage(tr('Request was wrong, try to update your script.'), tr('Posting Error')) return true elsif body.include? 'AUTH' # Secrets are wrong showMessage(tr('Pseudo or password are wrong. You have to configure the script.'), tr('Login Error')) return false else showMessage(tr('Something is wrong, but we don\'t know what. Sorry!'), tr('Server error')) return true end else showMessage('HTTP Code: ' + res.code.to_s, 'Debug') if $debug_http showMessage(tr('Something is wrong, but we don\'t know what. Sorry!'), tr('Server error')) return true end rescue showMessage(tr('Track informations were not sent to server.'), tr('Network error')) return true rescue Timeout::Error, TimeoutError showMessage(tr('Track informations were not sent to server.'), tr('Network error')) return true end end ########################################################################## # Adds a 'Comment current track' entry in Amarok's menu def addMenuEntry $menuEntry = [tr('AmarokNowPlaying'), tr('Post a "special comment" about this song')] `dcop amarok script addCustomMenuItem "#{ $menuEntry[0] }" "#{ $menuEntry[1] }"` end ########################################################################## # Removes the 'Comment current track' entry in Amarok's menu def removeMenuEntry `dcop amarok script removeCustomMenuItem "#{ $menuEntry[0] }" "#{ $menuEntry[1] }"` end ########################################################################## # Sends the current track to serveur. Returns false if and only if # secrets are wrong, else returns true, even if errors occured. def checkNewVersion url = URI.parse( $remoteAppBase.to_s + ( $remoteAppBase.to_s[-1] == '/' ? 'version.php' : '/version.php') ) req = Net::HTTP::Get.new(url.path) req['User-Agent'] = "AmarokNowPlaying Script #{ $version }" if ( ENV['http_proxy'].to_s().empty?() ) newhttp = Net::HTTP.new(url.host, url.port) else proxy_url = URI.parse(ENV['http_proxy']) if proxy_url.userinfo proxy_user, proxy_pass = proxy_url.userinfo.split(/:/) newhttp = Net::HTTP.new(url.host, url.port, proxy_url.host, proxy_url.port, proxy_user, proxy_pass) else newhttp = Net::HTTP.new(url.host, url.port, proxy_url.host, proxy_url.port) end end begin res = newhttp.start {|http| http.request(req) } case res when Net::HTTPSuccess body = res.body.to_s() showMessage(body.tr('"', "''"), 'Debug') if $debug_http remote_version = ( /~([0-9]+\.)+[0-9]+~/.match body ).to_s.tr('~', '') website = ( /~https?:\/\/.+~/.match body ).to_s.tr('~', '') # Compare version numbers begin remote_version = Version.new remote_version current_version = Version.new $version if remote_version > current_version showMessage(tr('A newer version of this script is available, check its website:') + ' http://codingteam.net/project/amaroknowplaying', tr('Update')) end rescue end else showMessage('HTTP Code: ' + res.code.to_s, 'Debug') if $debug_http showMessage(tr('Something is wrong, but we don\'t know what. Sorry!'), tr('Server error')) return true end rescue showMessage(tr('Cannot look for new version.'), tr('Network error')) rescue Timeout::Error, TimeoutError showMessage(tr('Cannot look for new version.'), tr('Network error')) end end # End of definitions, ACTION ! ########################################################################## # Main loop trap( 'SIGTERM' ) do # Cleanup when ending submitTrack( 'none' ) removeMenuEntry() end loadFiles() loadConfig() checkNewVersion() loop do message = gets().chomp() # Read message on stdin command = /[A-Za-z]*/.match( message ).to_s() case command when 'configure' loadConfig( true ) when 'customMenuClicked' submitTrack( 'special' ) when 'engineStateChange' submitTrack() end end