Advertisement
Guest User

Mikro

a guest
May 21st, 2016
108
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Python 22.88 KB | None | 0 0
  1. #!/usr/bin/env python3
  2.  
  3. import sys
  4. import os
  5. import tkinter
  6. from tkinter import ttk
  7. from tkinter import StringVar
  8. from PIL import ImageTk, Image
  9. from configparser import ConfigParser
  10. from threading import Thread
  11. from queue import Queue
  12. import traceback
  13. import vlc
  14. import logging
  15. import urllib.request
  16. import urllib.error
  17. import io
  18. import data
  19. from pandora import *
  20.  
  21. class Mikro:
  22.     def __init__(self):
  23.         self.root = tkinter.Tk(className='Mikro')
  24.         self.root.protocol('WM_DELETE_WINDOW', self.on_quit)
  25.         self.root.title('Mikro')
  26.         self.root.resizable(width='False', height='False')
  27.         self.pandora = Pandora()
  28.         self.prefs = Preferences(self)
  29.         self.worker = Worker(self).send
  30.         self.album_art = AlbumArt(self)
  31.         self.station = Station(self)
  32.         self.playlist = Playlist(self)
  33.         self.song = Song(self)
  34.         frame = ttk.Frame(self.root)
  35.         frame.pack(padx=8, pady=8, expand=True, fill='both')
  36.         self.info_label_text = StringVar()
  37.         self.info_label_text.set('')
  38.         self.play_pause_button_text = StringVar()
  39.         self.play_pause_button_text.set('Pause')
  40.         self.mute_button_text = StringVar()
  41.         self.mute_button_text.set('Mute')
  42.         self.love_button_text = StringVar()
  43.         self.love_button_text.set('Love')
  44.         self.stations_combo = ttk.Combobox(frame, justify='center', values='', state='readonly')
  45.         self.stations_combo.bind('<<ComboboxSelected>>', self.station.change)
  46.         self.stations_combo.pack(fill='x', expand=True)
  47.         top_frame = ttk.Frame(frame)
  48.         top_frame.pack(pady=8, expand=True, fill='x')
  49.         play_pause_button = ttk.Button(top_frame, textvariable=self.play_pause_button_text, command=self.play_pause_toggle)
  50.         play_pause_button.pack(side='left', expand=True, fill='x')
  51.         skip_button = ttk.Button(top_frame, text='Skip', command=self.user_skip)
  52.         skip_button.pack(side='left', expand=True, fill='x')
  53.         mute_button = ttk.Button(top_frame, textvariable=self.mute_button_text, command=self.mute_toggle)
  54.         mute_button.pack(side='left', expand=True, fill='x')
  55.         self.vol_slider = ttk.Scale(top_frame, from_=0, to=100, orient='horizontal', value='100', command=self.change_vol)
  56.         self.vol_slider.pack(padx=4, pady=0, side='left', expand=True, fill='x')
  57.         settings_button = ttk.Button(top_frame, text='Settings', command=self.mute_toggle)
  58.         settings_button.pack(side='left', expand=True, fill='x')
  59.         self.album_art_label = ttk.Label(frame, textvariable=self.info_label_text, image='', compound='left')
  60.         self.album_art_label.pack(expand=True, fill='x', pady=0, padx=0)
  61.         spacer_frame = ttk.Frame(frame)
  62.         spacer_frame.pack(expand=True, fill='x', pady=4)
  63.         button_frame = ttk.Frame(frame)
  64.         love_button = ttk.Button(button_frame, text='Love', textvariable=self.love_button_text, command=self.love_toggle)
  65.         love_button.pack(side='left', expand=True, fill='x')
  66.         tired_button = ttk.Button(button_frame, text='Tired', command=self.song.tired)
  67.         tired_button.pack(side='left', expand=True, fill='x')
  68.         ban_button = ttk.Button(button_frame, text='Ban', command=self.song.ban)
  69.         ban_button.pack(side='left', expand=True, fill='x')
  70.         button_frame.pack(expand=True, fill='x')
  71.         self.player = Player(self)
  72.         self.player.mute = False
  73.         self.vol_slider.set(self.prefs.volume)
  74.         self.time_loop = None
  75.         self.reconnect_time = None
  76.         self.pending_prefs_save = False
  77.         if not self.prefs.username or not self.prefs.password:
  78.             print('show prefs')
  79.         else:
  80.             self.connect(False)
  81.  
  82.     def connect(self, reconnecting):
  83.         if self.prefs.pandora_one:
  84.             client = data.client_keys['pandora-one']
  85.         else:
  86.             client = data.client_keys['android-generic']            
  87.         args = (client, self.prefs.username, self.prefs.password)
  88.         def reconnect(*ignore):
  89.             self.reconnect_time = time.time() + 1800
  90.             if not reconnecting:
  91.                 self.station.process()
  92.         self.worker(self.pandora.connect, args, reconnect)
  93.  
  94.     def start_time_loop(self):
  95.         def time_loop():
  96.             self.info_label_text.set(self.song.song_text)
  97.             self.time_loop = self.root.after(1000, time_loop)
  98.         if self.time_loop is None:
  99.             time_loop()
  100.  
  101.     def stop_time_loop(self):
  102.         if self.time_loop is not None:
  103.             self.root.after_cancel(self.time_loop)
  104.             self.time_loop = None
  105.  
  106.     def love_toggle(self):
  107.         if self.song.rating is None:
  108.             self.song.love()
  109.         else:
  110.             self.song.unrate()
  111.  
  112.     def user_skip(self):
  113.         if self.song.isAd:
  114.             return
  115.         self.song.next()
  116.  
  117.     def play_pause_toggle(self):
  118.         player_state = self.player.state
  119.         if player_state is not None:
  120.             self.player.state = not player_state
  121.            
  122.  
  123.     def on_play_pause(self, state):
  124.         if state:
  125.             self.play_pause_button_text.set('Pause')
  126.             self.start_time_loop()
  127.         else:
  128.             self.play_pause_button_text.set('Play')
  129.             self.stop_time_loop()
  130.  
  131.     def mute_toggle(self):
  132.         mute_state = self.player.mute
  133.         if mute_state is not None:
  134.             self.player.mute = not mute_state
  135.  
  136.     def on_mute_unmute(self, muted):
  137.         if muted:
  138.             self.mute_button_text.set('Unmute')
  139.         else:
  140.             self.mute_button_text.set('Mute')
  141.            
  142.     def change_vol(self, *ignore):
  143.         slider_value = round(self.vol_slider.get())
  144.         player_volume = self.player.volume
  145.         if slider_value != player_volume:
  146.             self.player.volume = slider_value
  147.             self.prefs.volume = slider_value
  148.  
  149.     def on_quit(self):
  150.         self.prefs.write()
  151.         self.root.destroy()
  152.        
  153. class Station:
  154.     def __init__(self, parent):
  155.         self._mikro = parent
  156.         self._pandora = self._mikro.pandora
  157.         self._index = None
  158.  
  159.     def process(self, *ignore):
  160.         stations = self._pandora.stations
  161.         last_station = self._mikro.prefs.last_station
  162.         stations_combo = self._mikro.stations_combo
  163.         station_names = [i.name for i in stations]
  164.         min_combo_width = len(max(station_names, key=len))
  165.         self._index = next(iter(stations.index(i) for i in stations if i.id == last_station), 0)        
  166.         stations_combo['values'] = station_names
  167.         stations_combo['width'] = min_combo_width      
  168.         stations_combo.current(self._index)
  169.         self._mikro.playlist.get(start = True)
  170.  
  171.     def change(self, *ignore):
  172.         selected_station_index = self._mikro.stations_combo.current()
  173.         if self._index != selected_station_index:
  174.             self._index = selected_station_index
  175.             self._mikro.prefs.last_station = self.current.id
  176.             self._mikro.playlist.get(start = True)
  177.         self._mikro.stations_combo.selection_clear()
  178.  
  179.     @property
  180.     def current(self):
  181.         return self._pandora.stations[self._index]
  182.  
  183. class Playlist:
  184.     def __init__(self, parent):
  185.         self._mikro = parent
  186.         self._worker = self._mikro.worker
  187.         self._station = self._mikro.station
  188.         self._prefs = self._mikro.prefs
  189.         self._waiting = True
  190.         self._playlist = []
  191.  
  192.     @property
  193.     def songs_remaining(self):
  194.         return len(self._playlist)
  195.  
  196.     @property
  197.     def waiting(self):
  198.         return self._waiting
  199.  
  200.     def pop_zero(self):  
  201.         return self._playlist.pop(0)
  202.  
  203.     def get(self, start = False):
  204.         if start:
  205.             self._waiting = True
  206.         def process_playlist(playlist):
  207.             if start:
  208.                 self._waiting = False
  209.                 self._playlist = playlist
  210.                 self._mikro.song.next()
  211.             else:
  212.                 self._playlist += playlist
  213.         self._worker(self._station.current.get_playlist, (), process_playlist)  
  214.  
  215. class Song:
  216.     def __init__(self, parent):
  217.         self._mikro = parent
  218.         self._root = self._mikro.root
  219.         self._worker = self._mikro.worker
  220.         self._pandora = self._mikro.pandora
  221.         self._album_art = self._mikro.album_art
  222.         self._playlist = self._mikro.playlist
  223.         self._station = self._mikro.station
  224.         self._prefs = self._mikro.prefs
  225.         self.__current = None
  226.         self.__duration = None
  227.         self._bitrate_codec_str =''
  228.  
  229.     @property
  230.     def _current(self):
  231.         return self.__current
  232.  
  233.     @_current.setter
  234.     def _current(self, new_song):
  235.         self.__current = new_song
  236.  
  237.     @property
  238.     def _time_to_reconnect(self):
  239.         return time.time() > self._mikro.reconnect_time
  240.  
  241.     def _register_ad(self):
  242.         self._worker(self._pandora.register_ad,
  243.                      (self._current.adTokens,
  244.                       self._station.current.id), )
  245.  
  246.     def _queue(self):
  247.         self._get_new_song()
  248.         self._album_art.get(self._current.artUrl)
  249.         self._set_description()
  250.         if self.isAd:
  251.             self._register_ad()
  252.             self._root.title('Commercial Advertisement - Mikro')
  253.         else:
  254.             self._root.title('%s by %s - Mikro'%(self._current.title, self._current.artist))
  255.         if self.rating is None:
  256.             self._mikro.love_button_text.set('Love')
  257.         else:
  258.             self._mikro.love_button_text.set('Unlove')
  259.         self._mikro.player.start_new_song(self._url)
  260.         if self._time_to_reconnect:
  261.             self._mikro.connect(True)
  262.         if self._playlist.songs_remaining == 0:
  263.             self._playlist.get()
  264.  
  265.     def next(self):
  266.         if not self._playlist.waiting:
  267.             self._mikro.stop_time_loop()
  268.             self._queue()
  269.  
  270.     def _get_new_song(self):
  271.         self._current = self._playlist.pop_zero()
  272.  
  273.     def love(self):
  274.         if self._current.isAd:
  275.             return
  276.         def set_button_text(*ignore):
  277.             self._mikro.love_button_text.set('Unlove')
  278.         self._worker(self._current.rate, (RATE_LOVE, self._station.current), set_button_text)
  279.  
  280.     def ban(self):
  281.         if self._current.isAd:
  282.             return
  283.         self._worker(self._current.rate, (RATE_BAN, self._station.current), )
  284.         self.next()
  285.  
  286.     def unrate(self):
  287.         if self._current.isAd:
  288.             return
  289.         def set_button_text(*ignore):
  290.             self._mikro.love_button_text.set('Love')
  291.         self._worker(self._current.rate, (RATE_NONE, self._station.current), set_button_text)
  292.  
  293.     def tired(self):
  294.         if self._current.isAd:
  295.             return
  296.         self._worker(self._current.set_tired, (), )
  297.         self.next()
  298.  
  299.     def _set_description(self):
  300.         if not self.isAd:
  301.             self._song_info = ' %s\n by %s\n from %s' %(self._current.title, self._current.artist, self._current.album)
  302.         else:
  303.             self._song_info = ' Commercial Advertisement\n from Pandora'
  304.         self._bitrate_codec_str = ' %skbit/s %s' %(self._birate, self._codec)
  305.  
  306.     @property
  307.     def rating(self):
  308.         return self._current.rating
  309.  
  310.     @property
  311.     def isAd(self):
  312.         return self._current.isAd
  313.  
  314.     @property
  315.     def song_text(self):
  316.         return '%s\n\n%s - %s / %s' %(self._song_info, self._bitrate_codec_str, self._position, self._duration)
  317.  
  318.     @property
  319.     def _url(self):
  320.         return self._current.audioUrlMap[self._prefs.audio_quality]['audioUrl']
  321.  
  322.     @property
  323.     def _birate(self):
  324.         return self._current.audioUrlMap[self._prefs.audio_quality]['bitrate']
  325.  
  326.     @property
  327.     def _codec(self):
  328.         return self._current.audioUrlMap[self._prefs.audio_quality]['encoding']
  329.  
  330.     @property
  331.     def _position(self):
  332.         position = self._mikro.player.position
  333.         if position is None:
  334.             return '--:--'
  335.         return self._format_time(position)
  336.        
  337.     @property
  338.     def _duration(self):
  339.         duration = self.__duration
  340.         if duration is None:
  341.             return '--:--'        
  342.         return duration
  343.  
  344.     def set_duration(self, dur):
  345.         if dur is None:
  346.             self.__duration = None
  347.             return
  348.         if not self._current.got_duration:
  349.             self.__current.got_duration = True
  350.             self.__duration = self._format_time(dur)
  351.  
  352.     def _format_time(self, milliseconds):
  353.         milliseconds /= 1000
  354.         seconds = milliseconds % 60
  355.         milliseconds /= 60
  356.         minutes = milliseconds % 60
  357.         milliseconds //= 60
  358.         hours = milliseconds
  359.         if hours:
  360.             return '%i:%02i:%02i'%(hours, minutes, seconds)
  361.         else:
  362.             return '%02i:%02i'%(minutes, seconds)
  363.  
  364. class AlbumArt:
  365.     def __init__(self, parent):
  366.         self._worker = Worker(parent).send
  367.         self._mikro = parent
  368.         self.__default_art = None
  369.  
  370.     def get(self, artUrl):
  371.         def get_albumArt():
  372.             if not artUrl:
  373.                 return self._default_art
  374.             try:
  375.                 with urllib.request.urlopen(artUrl) as albumArt:
  376.                     albumArt = io.BytesIO(albumArt.read())
  377.                 with Image.open(albumArt) as albumArt:
  378.                     albumArt = albumArt.resize((125, 125), Image.ANTIALIAS)
  379.                 albumArt = ImageTk.PhotoImage(albumArt)
  380.                 return albumArt
  381.             except urllib.error.HTTPError:
  382.                 return self._default_art
  383.         def push_albumArt(albumArt):
  384.             self._mikro.album_art_label.config(image='')
  385.             self._mikro.album_art_label.config(image=albumArt)
  386.         self._worker(get_albumArt, (), push_albumArt)
  387.  
  388.     @property
  389.     def _default_art(self):
  390.         if self.__default_art is not None:
  391.             return self.__default_art            
  392.         with Image.open('default.jpg') as defaultArt:
  393.             self.__default_art = ImageTk.PhotoImage(defaultArt)
  394.         return self.__default_art        
  395.  
  396. class Player:
  397.     def __init__(self, parent):
  398.         self._after_idle = parent.root.after_idle
  399.         self._set_duration = parent.song.set_duration
  400.         self._next_song = parent.song.next
  401.         self._on_play_pause = parent.on_play_pause
  402.         self._on_mute_unmute = parent.on_mute_unmute                  
  403.         self._vlc_instance = vlc.Instance()
  404.         self._player = self._vlc_instance.media_player_new()
  405.  
  406.         self._eventManager = self._player.event_manager()
  407.         self._eventManager.event_attach(vlc.EventType.MediaPlayerLengthChanged, self._on_LengthChanged)
  408.         self._eventManager.event_attach(vlc.EventType.MediaPlayerEndReached, self._on_EndReached)
  409.         self._eventManager.event_attach(vlc.EventType.MediaPlayerPlaying, self._on_Playing)
  410.         self._eventManager.event_attach(vlc.EventType.MediaPlayerPaused, self._on_Paused)
  411.         self._eventManager.event_attach(vlc.EventType.MediaPlayerMuted, self._on_Muted)
  412.         self._eventManager.event_attach(vlc.EventType.MediaPlayerUnmuted, self._on_Unmuted)
  413.  
  414.     def _on_LengthChanged(self, event):
  415.         self._after_idle(self._set_duration, self.duration)
  416.  
  417.     def _on_EndReached(self, event):
  418.         self._after_idle(self._next_song)
  419.  
  420.     def _on_Playing(self, event):
  421.         self._after_idle(self._on_play_pause, True)
  422.  
  423.     def _on_Paused(self, event):
  424.         self._after_idle(self._on_play_pause, False)
  425.  
  426.     def _on_Muted(self, event):
  427.         self._after_idle(self._on_mute_unmute, True)
  428.  
  429.     def _on_Unmuted(self, event):
  430.         self._after_idle(self._on_mute_unmute, False)
  431.  
  432.     @property
  433.     def position(self):
  434.         position = self._player.get_time()
  435.         if position < 0:
  436.             position = None  
  437.         return position
  438.  
  439.     @property
  440.     def duration(self):
  441.         duration = self._player.get_length()
  442.         if duration < 0:
  443.             duration = None
  444.         return duration
  445.  
  446.     @property
  447.     def state(self):
  448.         state = self._player.get_state()
  449.         if state == vlc.State.Playing:
  450.             return True
  451.         elif state == vlc.State.Paused:
  452.             return False
  453.         else:
  454.             return None
  455.  
  456.     @state.setter
  457.     def state(self, state):
  458.         if state:
  459.             self._player.play()
  460.         else:
  461.             self._player.pause()
  462.  
  463.     @property
  464.     def volume(self):
  465.         return self._player.audio_get_volume()
  466.  
  467.     @volume.setter
  468.     def volume(self, new_vol):
  469.         self._player.audio_set_volume(new_vol)
  470.  
  471.     @property
  472.     def _mute(self):
  473.         mute = self._player.audio_get_mute()
  474.         if mute == -1:
  475.             return None
  476.         elif mute == 0:
  477.             return False
  478.         elif mute == 1:
  479.             return True
  480.  
  481.     @property
  482.     def mute(self):
  483.         return self._mute
  484.  
  485.     @mute.setter
  486.     def mute(self, mute):
  487.         if self._mute is not None:
  488.             self._player.audio_set_mute(mute)
  489.  
  490.     def start_new_song(self, audioUrl):
  491.         song = self._vlc_instance.media_new_location(audioUrl)
  492.         self._player.set_media(song)
  493.         song.release()
  494.         self._player.play()
  495.  
  496. class Preferences:
  497.     def __init__(self, parent):
  498.         self._mikro = parent
  499.         self._root = self._mikro.root
  500.         self._config_parser = ConfigParser()
  501.         self._prefs_file = None
  502.         self._username = None
  503.         self._password = None
  504.         self._pandora_one = None
  505.         self._last_station = None
  506.         self._audio_quality = None
  507.         self._volume = None
  508.         self._quality_combo_values = None
  509.         self._default_username = 'jasonlevigray3@gmail.com'
  510.         self._default_password = 'notachance'
  511.         self._default_pandora_one = True
  512.         self._default_last_station = ''
  513.         self._default_audio_quality = 'highQuality'
  514.         self._default_volume = 100
  515.         self._default_quality_combo_values = ['128kbit/s mp3',
  516.                                               '64kbit/s aacplus',
  517.                                               '32kbit/s aacplus']
  518.         self.valid_audio_qualities = ['highQuality',
  519.                                       'mediumQuality',
  520.                                       'lowQuality']
  521.         self._prepare_prefs()
  522.  
  523.     @property
  524.     def username(self):
  525.         return self._username or self._default_username
  526.  
  527.     @username.setter
  528.     def username(self, username):
  529.         if isinstance(username, str):
  530.             self._username = username
  531.  
  532.     @property
  533.     def password(self):
  534.         return self._password or self._default_password
  535.  
  536.     @password.setter
  537.     def password(self, password):
  538.         if isinstance(password, str):
  539.             self._password = password
  540.  
  541.     @property
  542.     def pandora_one(self):
  543.         return self._pandora_one or self._default_pandora_one
  544.  
  545.     @pandora_one.setter
  546.     def pandora_one(self, pandora_one):
  547.         if isinstance(pandora_one, bool):
  548.             self._pandora_one = pandora_one
  549.             self.quality_combo_values = pandora_one
  550.  
  551.     @property
  552.     def audio_quality(self):
  553.         return self._audio_quality or self._default_audio_quality
  554.  
  555.     @audio_quality.setter
  556.     def audio_quality(self, audio_quality):
  557.         if isinstance(audio_quality, str):
  558.             if audio_quality in self.valid_audio_qualities:
  559.                 self._audio_quality = audio_quality
  560.  
  561.     @property
  562.     def last_station(self):
  563.         return self._last_station or self._default_last_station
  564.  
  565.     @last_station.setter
  566.     def last_station(self, last_station):
  567.         if isinstance(last_station, str):
  568.             self._last_station = last_station
  569.  
  570.     @property
  571.     def volume(self):
  572.         return self._volume or self._default_volume
  573.  
  574.     @volume.setter
  575.     def volume(self, volume):
  576.         if isinstance(volume, int):
  577.             self._volume = volume
  578.  
  579.     @property
  580.     def quality_combo_values(self):
  581.         return self._quality_combo_values or self._default_quality_combo_values
  582.  
  583.     @quality_combo_values.setter
  584.     def quality_combo_values(self, pandora_one):
  585.         if isinstance(pandora_one, bool):
  586.             if pandora_one:
  587.                 high = '192kbit/s mp3'
  588.             else:
  589.                 high = '128kbit/s mp3'
  590.             self._quality_combo_values = [high,
  591.                                           '64kbit/s aacplus',
  592.                                           '32kbit/s aacplus']
  593.  
  594.     def _prepare_prefs(self):
  595.         prefs_dir = '%s/.config/mikro' %os.path.expanduser('~')
  596.         if not os.path.exists(prefs_dir):
  597.             os.makedirs(prefs_dir)
  598.         prefs_file = 'config.ini'
  599.         self._prefs_file = '%s/%s' %(prefs_dir, prefs_file)
  600.         if not os.path.isfile(self._prefs_file):
  601.             self.write()
  602.         else:
  603.             self.read()
  604.  
  605.     def read(self):
  606.         with open(self._prefs_file, 'r') as prefs_file:
  607.             self._config_parser.readfp(prefs_file)
  608.             setting = self._config_parser['SETTINGS']
  609.             self.username = setting.get('username', '')
  610.             self.password = setting.get('password', '')
  611.             self.pandora_one = setting.getboolean('pandora_one', False)
  612.             self.last_station = setting.get('last_station', '')
  613.             self.audio_quality = setting.get('audio_quality', 'highQuality')
  614.             self.volume = int(setting.get('volume', '100'))
  615.  
  616.     def write(self):
  617.         self._config_parser['SETTINGS'] = {'username': self.username,
  618.                                            'password': self.password,
  619.                                            'pandora_one': str(self.pandora_one),
  620.                                            'last_station': self.last_station,
  621.                                            'audio_quality':self.audio_quality,
  622.                                            'volume': str(self.volume)}
  623.         with open(self._prefs_file, 'w') as prefs_file:
  624.             self._config_parser.write(prefs_file)          
  625.  
  626. class Worker:
  627.     def __init__(self, parent):
  628.         self._root = parent.root
  629.         self._thread = Thread(target=self._run)
  630.         self._thread.daemon = True
  631.         self._queue = Queue()
  632.         self._thread.start()
  633.        
  634.     def _run(self):
  635.         while True:
  636.             command, args, callback, errorback = self._queue.get()
  637.             try:
  638.                 result = command(*args)
  639.                 if callback:
  640.                     self._root.after_idle(callback, result)
  641.             except Exception as e:
  642.                 e.traceback = traceback.format_exc()
  643.                 if errorback:
  644.                     self._root.after_idle(errorback, e)
  645.                
  646.     def send(self, command, args=(), callback=None, errorback=None):
  647.         if errorback is None: errorback = self._default_errorback
  648.         self._queue.put((command, args, callback, errorback))
  649.        
  650.     def _default_errorback(self, error):
  651.         print('Unhandled exception in worker thread:\n{}'.format(error.traceback))
  652.  
  653. def main():
  654.     Mikro().root.mainloop()
  655.     sys.exit(0)
  656.  
  657. if __name__ == '__main__':
  658.     main()
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement