]> git.0d.be Git - botaradio.git/blob - media/url.py
fix: some small issue
[botaradio.git] / media / url.py
1 import threading
2 import logging
3 import os
4 import hashlib
5 import traceback
6 from PIL import Image
7 import youtube_dl
8 import glob
9
10 import constants
11 import media
12 import variables as var
13 from media.file import FileItem
14 import media.system
15
16 log = logging.getLogger("bot")
17
18
19 class URLItem(FileItem):
20     def __init__(self, bot, url, from_dict=None):
21         self.validating_lock = threading.Lock()
22         if from_dict is None:
23             self.url = url
24             self.title = ''
25             self.duration = 0
26             self.ready = 'pending'
27             super().__init__(bot, "")
28             self.id = hashlib.md5(url.encode()).hexdigest()
29             path = var.tmp_folder + self.id + ".mp3"
30
31             if os.path.isfile(path):
32                 self.log.info("url: file existed for url %s " % self.url)
33                 self.ready = 'yes'
34                 self.path = path
35                 self._get_info_from_tag()
36             else:
37                 # self._get_info_from_url()
38                 pass
39         else:
40             super().__init__(bot, "", from_dict)
41             self.url = from_dict['url']
42             self.duration = from_dict['duration']
43
44         self.downloading = False
45         self.type = "url"
46
47     def uri(self):
48         return self.path
49
50     def is_ready(self):
51         if self.downloading or self.ready != 'yes':
52             return False
53         if self.ready == 'yes' and not os.path.exists(self.path):
54             self.log.info(
55                 "url: music file missed for %s" % self.format_debug_string())
56             self.ready = 'validated'
57             return False
58
59         return True
60
61     def validate(self):
62         if self.ready in ['yes', 'validated']:
63             return True
64
65         if os.path.exists(self.path):
66             self.ready = "yes"
67             return True
68
69         # avoid multiple process validating in the meantime
70         self.validating_lock.acquire()
71         info = self._get_info_from_url()
72         self.validating_lock.release()
73
74         if self.duration == 0 and not info:
75             return False
76
77         if self.duration > var.config.getint('bot', 'max_track_duration') != 0:
78             # Check the length, useful in case of playlist, it wasn't checked before)
79             log.info(
80                 "url: " + self.url + " has a duration of " + str(self.duration) + " min -- too long")
81             self.send_client_message(constants.strings('too_long'))
82             return False
83         else:
84             self.ready = "validated"
85             return True
86
87     # Run in a other thread
88     def prepare(self):
89         if not self.downloading:
90             assert self.ready == 'validated'
91             return self._download()
92         else:
93             assert self.ready == 'yes'
94             return True
95
96     def _get_info_from_url(self):
97         self.log.info("url: fetching metadata of url %s " % self.url)
98         ydl_opts = {
99             'noplaylist': True
100         }
101         succeed = False
102         with youtube_dl.YoutubeDL(ydl_opts) as ydl:
103             attempts = var.config.getint('bot', 'download_attempts', fallback=2)
104             for i in range(attempts):
105                 try:
106                     info = ydl.extract_info(self.url, download=False)
107                     self.duration = info['duration'] / 60
108                     self.title = info['title']
109                     succeed = True
110                     return True
111                 except youtube_dl.utils.DownloadError:
112                     pass
113
114         if not succeed:
115             self.ready = 'failed'
116             self.log.error("url: error while fetching info from the URL")
117             self.send_client_message(constants.strings('unable_download'))
118             return False
119
120     def _download(self):
121         media.system.clear_tmp_folder(var.tmp_folder, var.config.getint('bot', 'tmp_folder_max_size'))
122
123         self.downloading = True
124         base_path = var.tmp_folder + self.id
125         save_path = base_path + ".%(ext)s"
126         mp3_path = base_path + ".mp3"
127
128         # Download only if music is not existed
129         self.ready = "preparing"
130
131         self.log.info("bot: downloading url (%s) %s " % (self.title, self.url))
132         ydl_opts = ""
133
134         ydl_opts = {
135             'format': 'bestaudio/best',
136             'outtmpl': save_path,
137             'noplaylist': True,
138             'writethumbnail': True,
139             'updatetime': False,
140             'postprocessors': [{
141                 'key': 'FFmpegExtractAudio',
142                 'preferredcodec': 'mp3',
143                 'preferredquality': '192'},
144                 {'key': 'FFmpegMetadata'}]
145         }
146
147         with youtube_dl.YoutubeDL(ydl_opts) as ydl:
148             attempts = var.config.getint('bot', 'download_attempts', fallback=2)
149             download_succeed = False
150             for i in range(attempts):
151                 self.log.info("bot: download attempts %d / %d" % (i+1, attempts))
152                 try:
153                     info = ydl.extract_info(self.url)
154                     download_succeed = True
155                     break
156                 except:
157                     error_traceback = traceback.format_exc().split("During")[0]
158                     error = error_traceback.rstrip().split("\n")[-1]
159                     self.log.error("bot: download failed with error:\n %s" % error)
160
161             if download_succeed:
162                 self.path = mp3_path
163                 self.ready = "yes"
164                 self.log.info(
165                     "bot: finished downloading url (%s) %s, saved to %s." % (self.title, self.url, self.path))
166                 self.downloading = False
167                 self._read_thumbnail_from_file(base_path + ".jpg")
168                 return True
169             else:
170                 for f in glob.glob(base_path + "*"):
171                     os.remove(f)
172                 self.send_client_message(constants.strings('unable_download'))
173                 self.ready = "failed"
174                 self.downloading = False
175                 return False
176
177     def _read_thumbnail_from_file(self, path_thumbnail):
178         if os.path.isfile(path_thumbnail):
179             im = Image.open(path_thumbnail)
180             self.thumbnail = self._prepare_thumbnail(im)
181
182     def to_dict(self):
183         dict = super().to_dict()
184         dict['type'] = 'url'
185         dict['url'] = self.url
186         dict['duration'] = self.duration
187
188         return dict
189
190
191     def format_debug_string(self):
192         return "[url] {title} ({url})".format(
193             title=self.title,
194             url=self.url
195         )
196
197     def format_song_string(self, user):
198         return constants.strings("url_item",
199                                     title=self.title,
200                                     url=self.url,
201                                     user=user)
202
203     def format_current_playing(self, user):
204         display = constants.strings("now_playing", item=self.format_song_string(user))
205
206         if self.thumbnail:
207             thumbnail_html = '<img width="80" src="data:image/jpge;base64,' + \
208                              self.thumbnail + '"/>'
209             display += "<br />" +  thumbnail_html
210
211         return display
212
213     def format_short_string(self):
214         return self.title if self.title else self.url
215
216     def display_type(self):
217         return constants.strings("url")