HOTFIX, added missing option declaration.
[i_like_pandora.git] / likes_pandora.py
1 #!/usr/bin/env python
2 # -*- coding: utf-8 -*-
3
4 __author__ = ("Dylan Lloyd <dylan@psu.edu>")
5 __license__ = "BSD"
6
7 default_options = {
8 'notifications' : 'true',
9 # NOTIFICATIONS must be a string due to issues noted here:
10 # http://bugs.python.org/issue974019
11 # ConfigParser.getboolean fails when falling back to the default value
12 # if the value type is bool.
13 'youtube-dl' : '/usr/bin/youtube-dl',
14 'default_icon' : '/usr/share/icons/gnome/48x48/mimetypes/gnome-mime-application-x-shockwave-flash.png',
15 'youtube-dl_options' : '--no-progress --ignore-errors --continue --max-quality=22 -o "%(stitle)s---%(id)s.%(ext)s"'
16 }
17
18 import ConfigParser # This module has been renamed to configparser in python 3.0
19 import sys
20 import os
21
22 CONFIG_FILE= os.path.join(os.path.expanduser('~'), '.i_like_pandora.config')
23 config = ConfigParser.ConfigParser(default_options)
24 loaded_files = config.read(CONFIG_FILE) # config.read returns an empty array if it fails.
25 if len(loaded_files) == 0:
26 print 'Can\'t find a configuration file at', CONFIG_FILE
27 sys.exit()
28 try:
29 USER = config.get('settings', 'username')
30 DIR = os.path.expanduser(config.get('settings', 'download_folder'))
31 NOTIFICATIONS = config.getboolean('settings', 'notifications')
32 YT_DL = config.get('settings', 'youtube-dl')
33 DEFAULT_ICON = config.get('settings', 'default_icon')
34 YT_OPT = default_options['youtube-dl_options']
35 except:
36 print 'There is a formatting error in the configuration file at', CONFIG_FILE
37 sys.exit()
38
39 from BeautifulSoup import BeautifulSoup
40 import urllib
41 import urllib2
42 import re
43 import copy
44 import shlex, subprocess
45
46 if NOTIFICATIONS:
47 import pynotify
48 import hashlib
49 import tempfile
50 import string
51
52 def fetch_stations(user):
53 """ This takes a pandora username and returns the a list of the station tokens that the user is subscribed to. """
54 stations = []
55 page = urllib.urlopen('http://www.pandora.com/favorites/profile_tablerows_station.vm?webname=' + USER)
56 page = BeautifulSoup(page)
57 table = page.findAll('div', attrs={'class':'station_table_row'})
58 for row in table:
59 if row.find('a'):
60 for attr, value in row.find('a').attrs:
61 if attr == 'href':
62 stations.append(value[10:])
63 return stations
64
65 def fetch_tracks(stations):
66 """ Takes a list of station tokens and returns a list of Title + Artist strings.
67 """
68 search_strings = []
69 for station in stations:
70 page = urllib.urlopen('http://www.pandora.com/favorites/station_tablerows_thumb_up.vm?token=' + station + '&sort_col=thumbsUpDate')
71 page = BeautifulSoup(page)
72 titles = []
73 artists = []
74 for span in page.findAll('span', attrs={'class':'track_title'}):
75 for attr, value in span.attrs:
76 if attr == 'tracktitle':
77 titles.append(value)
78 for anchor in page.findAll('a'):
79 artists.append(anchor.string)
80 if len(titles) == len(artists):
81 i = 0
82 for title in titles:
83 search_string = title + ' ' + artists[i]
84 search_strings.append(search_string)
85 i += 1
86 else:
87 # This would mean something strange has happened: there
88 # aren't the same number of titles and artist names on a
89 # station page.
90 pass
91 return search_strings
92
93 def search_youtube(search_strings):
94 """ This takes a list of search strings and tries to find the first result. It returns a list of the youtube video ids of those results.
95 """
96 video_list = []
97 for search_string in search_strings:
98 search_url = 'http://youtube.com/results?search_query=' + urllib.quote_plus(search_string)
99 page = urllib.urlopen(search_url)
100 page = BeautifulSoup(page)
101 result = page.find('div', attrs={'class':'video-main-content'})
102 if result == None:
103 print 'odd feedback for search, could not find div at ', search_url
104 continue
105 for attr, value in result.attrs:
106 if attr == 'id' and len(value[19:]) == 11:
107 video_list.append(value[19:])
108 elif attr == 'id':
109 print 'odd feedback for search', search_url, " : ", value[19:]
110 return video_list
111
112
113 def check_for_existing(video_list):
114 """ Checks the download-folder for existing videos with same id and removes from video_list. """
115 filelist = os.listdir(DIR)
116 i = 0
117 for video in copy.deepcopy(video_list):
118 for files in filelist:
119 if re.search(video,files):
120 del video_list[i]
121 i -= 1
122 i += 1
123 return video_list
124
125 def fetch_videos(video_list):
126 """ Uses subprocess to trigger a download using youtube-dl of the list created earlier, and triggers notifications if enabled. """
127 os.chdir(DIR)
128 args = shlex.split(YT_DL + ' ' + YT_OPT)
129 if NOTIFICATIONS: regex = re.compile("\[download\] Destination: (.+)")
130 for video in video_list:
131 if video:
132 thread = subprocess.Popen(args + ["http://youtube.com/watch?v=" + video], stdout=subprocess.PIPE)
133 output = thread.stdout.read()
134 if NOTIFICATIONS:
135 video_file = regex.findall(output)
136 if len(video_file) == 0:
137 break
138 thumbnail = hashlib.md5('file://' + DIR + video_file[0]).hexdigest() + '.png'
139 # Two '/'s instead of three because the path is
140 # absolute; I'm not sure how this'd work on windows.
141 title, sep, vid_id = video_file[0].rpartition('---')
142 title = string.replace(title, '_', ' ')
143 thumbnail = os.path.join(os.path.expanduser('~/.thumbnails/normal'), thumbnail)
144 if not os.path.isfile(thumbnail):
145 opener = urllib2.build_opener()
146 try:
147 page = opener.open('http://img.youtube.com/vi/' + video + '/1.jpg')
148 thumb = page.read()
149 # The thumbnail really should be saved to
150 # ~/.thumbnails/normal (Thumbnail Managing
151 # Standard)
152 # [http://jens.triq.net/thumbnail-spec/]
153 # As others have had problems anyway
154 # (http://mail.gnome.org/archives/gnome-list/2010-October/msg00009.html)
155 # I decided not to bother at the moment.
156 temp = tempfile.NamedTemporaryFile(suffix='.jpg')
157 temp.write(thumb)
158 temp.flush()
159 note = pynotify.Notification(title, 'video downloaded', temp.name)
160 except:
161 note = pynotify.Notification(title, 'video downloaded', DEFAULT_ICON)
162 else:
163 # Generally, this will never happen, because the
164 # video is a new file.
165 note = pynotify.Notification(title, 'video downloaded', thumbnail)
166 note.show()
167
168 def main():
169 stations = fetch_stations(USER)
170 if len(stations) == 0:
171 print 'Are you sure your pandora profile is public? Can\'t seem to find any stations listed with your account.'
172 search_strings = fetch_tracks(stations)
173 videos = search_youtube(search_strings)
174 videos = check_for_existing(videos)
175 fetch_videos(videos)
176
177 if __name__ == "__main__":
178 main()