playlistalo.py 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662
  1. #!/usr/bin/env python3
  2. #Playlistalo - simpatico script che legge le cartelle e genera la playlist
  3. #Requirements: youtube_dl Mastodon.py python-telegram-bot validators python-musicpd
  4. #import youtube_dlc as youtube_dl
  5. import youtube_dl
  6. import shutil
  7. import sys
  8. import re
  9. import os
  10. import validators
  11. from glob import glob
  12. import json
  13. import time
  14. import subprocess
  15. import random
  16. import configparser
  17. import hashlib
  18. from urllib.parse import urlparse, parse_qs
  19. from musicpd import MPDClient
  20. from telegram.ext import Updater, MessageHandler, Filters
  21. from mastodon import Mastodon, StreamListener
  22. scriptpath = os.path.dirname(os.path.realpath(__file__))
  23. #Crea le cartelle
  24. os.makedirs("playlist", exist_ok=True)
  25. os.makedirs("fallback", exist_ok=True)
  26. SHUFFLEUSERS = False
  27. SHUFFLESONGS = False
  28. SHUFFLEFALLBACK = False
  29. ARCHIVE = True
  30. TELEGRAM_TOKEN = ""
  31. MASTODON_TOKEN = ""
  32. MASTODON_URL = ""
  33. ANNOUNCEREPEAT = 3
  34. #Scrivi la prima configurazione
  35. configfile = 'playlistalo.conf'
  36. if not os.path.exists(configfile):
  37. config = configparser.ConfigParser()
  38. config['playlistalo'] = {'ShuffleUsers': SHUFFLEUSERS, 'ShuffleSongs': SHUFFLESONGS, 'ShuffleFallback': SHUFFLEFALLBACK, 'Archive': ARCHIVE, 'Telegram_token': TELEGRAM_TOKEN, 'Mastodon_token': MASTODON_TOKEN, 'Mastodon_url': MASTODON_URL}
  39. with open(configfile, 'w') as f:
  40. config.write(f)
  41. #Leggi la configurazione
  42. config = configparser.ConfigParser()
  43. config.read(configfile)
  44. playlistaloconf = config['playlistalo']
  45. SHUFFLEUSERS = playlistaloconf.getboolean('ShuffleUsers', SHUFFLEUSERS)
  46. SHUFFLESONGS = playlistaloconf.getboolean('ShuffleSongs', SHUFFLESONGS)
  47. SHUFFLEFALLBACK = playlistaloconf.getboolean('ShuffleFallback', SHUFFLEFALLBACK)
  48. ARCHIVE = playlistaloconf.getboolean('Archive', ARCHIVE)
  49. TELEGRAM_TOKEN = playlistaloconf.get('Telegram_token', TELEGRAM_TOKEN)
  50. MASTODON_TOKEN = playlistaloconf.get('Mastodon_token',MASTODON_TOKEN)
  51. MASTODON_URL = playlistaloconf.get('Mastodon_url', MASTODON_URL)
  52. def video_id(value):
  53. """
  54. Examples:
  55. - http://youtu.be/SA2iWivDJiE
  56. - http://www.youtube.com/watch?v=_oPAwA_Udwc&feature=feedu
  57. - http://www.youtube.com/embed/SA2iWivDJiE
  58. - http://www.youtube.com/v/SA2iWivDJiE?version=3&hl=en_US
  59. """
  60. query = urlparse(value)
  61. if query.hostname == 'youtu.be':
  62. return query.path[1:]
  63. if query.hostname in ('www.youtube.com', 'm.youtube.com', 'youtube.com'):
  64. if query.path == '/watch':
  65. p = parse_qs(query.query)
  66. return p['v'][0]
  67. if query.path[:7] == '/embed/':
  68. return query.path.split('/')[2]
  69. if query.path[:3] == '/v/':
  70. return query.path.split('/')[2]
  71. # fail?
  72. return None
  73. def addurl(url, user = "-unknown-"):
  74. #print ('--- Inizio ---')
  75. os.makedirs("cache", exist_ok=True)
  76. ydl_opts = {
  77. 'format': 'bestaudio[ext=m4a]',
  78. 'outtmpl': 'cache/%(id)s.m4a',
  79. 'no-cache-dir': True,
  80. 'noplaylist': True,
  81. 'quiet': True,
  82. }
  83. url = url.strip()
  84. print ("url: " + url)
  85. print ("user: " + user)
  86. if not validators.url(url):
  87. print ('--- URL malformato ---')
  88. return ("Err: url non valido")
  89. #trova l'id dall'url
  90. id = video_id(url)
  91. if id:
  92. #cerca se ho gia' il json e il file
  93. if not (glob(os.path.join("cache", id + ".json")) and glob(os.path.join("cache", id + ".m4a"))):
  94. id = None
  95. else:
  96. #legge le info
  97. with open(os.path.join("cache", id + ".json")) as infile:
  98. j = json.load(infile)
  99. title = normalizetext(j['title'])
  100. track = normalizetext(j['track'])
  101. artist = normalizetext(j['artist'])
  102. album = normalizetext(j['album'])
  103. filetemp = os.path.join("cache", id + ".m4a")
  104. print ('id: %s' %(id))
  105. print ('title: %s' %(title))
  106. if not id:
  107. with youtube_dl.YoutubeDL(ydl_opts) as ydl:
  108. try:
  109. meta = ydl.extract_info(url, download = False)
  110. except youtube_dl.DownloadError as detail:
  111. print ('--- Errore video non disponibile ---')
  112. print(str(detail))
  113. return ("Err: " + str(detail))
  114. id = meta.get('id').strip()
  115. title = normalizetext(meta.get('title'))
  116. track = normalizetext(meta.get('track'))
  117. artist = normalizetext(meta.get('artist'))
  118. album = normalizetext(meta.get('album'))
  119. print ('id: %s' %(id))
  120. print ('title: %s' %(title))
  121. #scrivo il json
  122. with open(os.path.join("cache", id + ".json"), 'w') as outfile:
  123. json.dump(meta, outfile, indent=4)
  124. #ho letto le info, ora controllo se il file esiste altrimenti lo scarico
  125. #miglioria: controllare se upload_date e' uguale a quella del json gia' esistente
  126. filetemp = os.path.join("cache", id + ".m4a")
  127. if not glob(filetemp):
  128. print ('--- Scarico ---')
  129. ydl.download([url]) #non ho capito perche' ma senza [] fa un carattere per volta
  130. if not os.path.isfile(filetemp):
  131. return("Err: file non scaricato")
  132. return(add(filetemp, user, title, id, track, artist, album))
  133. def md5(fname):
  134. hash_md5 = hashlib.md5()
  135. with open(fname, "rb") as f:
  136. for chunk in iter(lambda: f.read(4096), b""):
  137. hash_md5.update(chunk)
  138. return hash_md5.hexdigest()
  139. def add(filetemp, user = "-unknown-", title = None, id = None, track = None, artist = None, album = None):
  140. if not id:
  141. id = md5(filetemp)
  142. if not title:
  143. title = os.path.splitext(os.path.basename(filetemp))[0]
  144. if not track:
  145. track = ''
  146. if not artist:
  147. artist = ''
  148. if not album:
  149. album = ''
  150. #se il file esiste gia' in playlist salto (potrebbe esserci, anche rinominato)
  151. if glob("playlist/**/*|" + id + ".*"):
  152. print ('--- File già presente ---')
  153. return ("Err: %s [%s] già presente" %(title, id))
  154. os.makedirs("playlist/" + user, exist_ok=True)
  155. #qui compone il nome del file
  156. fnam = (artist + " - " + track).strip() + "|" + title + "|" + id + ".m4a"
  157. if SHUFFLESONGS:
  158. fileout = str(random.randrange(10**6)).zfill(14) + "|" + fnam
  159. else:
  160. fileout = time.strftime("%Y%m%d%H%M%S") + "|" + fnam
  161. fileout = os.path.join("playlist/" + user, fileout)
  162. #se il file esiste gia' in archive prendo quello
  163. if glob("archive/*|" + id + ".*"):
  164. print ('--- File in archivio ---')
  165. copyfile(glob("archive/*|" + id + ".*")[0], fileout)
  166. else:
  167. print ('--- Converto ---')
  168. print (fileout)
  169. subprocess.call([scriptpath + "/trimaudio.sh", filetemp, fileout])
  170. print ('--- Fine ---')
  171. if not os.path.isfile(fileout):
  172. return("Err: file non convertito")
  173. if ARCHIVE:
  174. os.makedirs("archive", exist_ok=True)
  175. if not glob("archive/*|" + id + ".*"):
  176. shutil.copy2(fileout, "archive")
  177. #cerca la posizione del pezzo appena inserito
  178. pos = getposition(fileout)
  179. return ("OK: %s [%s] aggiunto alla playlist in posizione #%s" %(title, id, pos))
  180. def normalizetext(s):
  181. if s is None:
  182. return None
  183. else:
  184. s = re.sub(r'[\\|/|:|*|?|"|<|>|\|]',r'',s)
  185. s = " ".join(s.split())
  186. return s
  187. def listplaylist():
  188. pl = []
  189. pl2 = []
  190. for udir in sorted(glob("playlist/*/")):
  191. #print (udir)
  192. user = os.path.basename(os.path.dirname(udir))
  193. #cerca il file last
  194. last = ""
  195. if os.path.exists(udir + "/last"):
  196. f = open(udir + "/last", "r")
  197. last = f.readline().rstrip()
  198. else:
  199. files = [x for x in sorted(glob(udir + "/*")) if not os.path.basename(x) == "last"]
  200. if files:
  201. last = os.path.basename(files[0]).split("|")[0]
  202. #print ("LAST: " + last)
  203. #leggi i file nella cartella
  204. files = [x for x in sorted(glob(udir + "/*")) if not os.path.basename(x) == "last"]
  205. seq = 0
  206. for file in files:
  207. bn = os.path.splitext(os.path.basename(file))[0]
  208. #print ("BASENAME: " + bn)
  209. seq = seq + 1
  210. dat = bn.split("|")[0]
  211. if len(bn.split("|")) < 4:
  212. nam = bn.split("|")[1]
  213. cod = bn.split("|")[2]
  214. else:
  215. nam = bn.split("|")[2]
  216. cod = bn.split("|")[3]
  217. key = "-".join([str(seq).zfill(5), last, dat])
  218. #print ("KEY: " + key)
  219. plsong = [key, file, user, nam, cod]
  220. pl.append(plsong)
  221. pl.sort()
  222. #rimuove la prima colonna, che serve solo per l'ordinamento
  223. pl2 = [x[1:] for x in pl]
  224. #print (pl)
  225. #print ('\n'.join([", ".join(x) for x in pl]))
  226. #print ('\n'.join([x[0] for x in pl]))
  227. return pl2
  228. def listfallback():
  229. pl = []
  230. pl2 = []
  231. #leggi i file nella cartella
  232. files = [x for x in sorted(glob("fallback/*")) if not os.path.basename(x) == "last"]
  233. seq = 0
  234. for file in files:
  235. bn = os.path.splitext(os.path.basename(file))[0]
  236. seq = seq + 1
  237. dat = bn.split("|")[0]
  238. if len(bn.split("|")) < 4:
  239. nam = bn.split("|")[1]
  240. cod = bn.split("|")[2]
  241. else:
  242. nam = bn.split("|")[2]
  243. cod = bn.split("|")[3]
  244. key = "-".join([str(seq).zfill(5), dat])
  245. plsong = [key, file, "fallback", nam, cod]
  246. pl.append(plsong)
  247. pl.sort()
  248. #rimuove la prima colonna, che serve solo per l'ordinamento
  249. pl2 = [x[1:] for x in pl]
  250. return pl2
  251. def playlocal():
  252. if SHUFFLEUSERS:
  253. shuffleusers()
  254. if SHUFFLEFALLBACK:
  255. shufflefallback()
  256. while True:
  257. plt = listtot(1)
  258. if plt:
  259. song = plt[0][0]
  260. print(song)
  261. #qui fa play
  262. subprocess.call(["mplayer", "-nolirc", "-msglevel", "all=0:statusline=5", song])
  263. consume(song)
  264. def clean():
  265. #cancella tutto dalla playlist
  266. shutil.rmtree("playlist")
  267. os.makedirs("playlist")
  268. def shuffleusers():
  269. #scrivere un numero casuale dentro a tutti i file last
  270. for udir in sorted(glob("playlist/*/")):
  271. #print (udir)
  272. with open(udir + "/last", "w") as f:
  273. f.write(str(random.randrange(10**6)).zfill(14))
  274. def shufflefallback():
  275. #rinominare con un numero casuale i file in fallback
  276. files = [x for x in glob("fallback/*") if not os.path.basename(x) == "last"]
  277. for file in files:
  278. fname = str(random.randrange(10**6)).zfill(14) + "|" + "|".join(os.path.basename(file).split("|")[1:])
  279. fname = os.path.dirname(file) + "/" + fname
  280. os.rename(file, fname)
  281. def getposition(file):
  282. pl = listplaylist()
  283. try:
  284. return([x[0] for x in pl].index(file) + 1)
  285. except:
  286. pass
  287. def telegram_msg_parser(bot, update):
  288. print("Messaggio ricevuto")
  289. msg = update.message.text
  290. id = str(update.message.from_user.id)
  291. username = update.message.from_user.username
  292. urls = re.findall('http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@.&+]|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+', msg)
  293. user = "t_" + "-".join([i for i in [id, username] if i])
  294. #print (urls)
  295. #print (user)
  296. if not urls:
  297. update.message.reply_text("Non ho trovato indirizzi validi...")
  298. return()
  299. update.message.reply_text("Messaggio ricevuto. Elaboro...")
  300. for url in urls:
  301. #update.message.reply_text("Scarico %s" %(url))
  302. # start the download
  303. dl = addurl(url, user)
  304. update.message.reply_text(dl)
  305. def telegram_bot():
  306. print ("Bot avviato")
  307. # Create the EventHandler and pass it your bot's token.
  308. updater = Updater(TELEGRAM_TOKEN)
  309. # Get the dispatcher to register handlers
  310. dp = updater.dispatcher
  311. # parse message
  312. dp.add_handler(MessageHandler(Filters.text, telegram_msg_parser))
  313. # Start the Bot
  314. updater.start_polling()
  315. # Run the bot until you press Ctrl-C
  316. updater.idle()
  317. class MastodonListener(StreamListener):
  318. # andiamo a definire il metodo __init__, che prende una istanza di Mastodon come parametro opzionale e lo setta nella prop. self.mastodon
  319. def __init__(self, mastodonInstance=None):
  320. self.mastodon = mastodonInstance
  321. def on_notification(self, notification):
  322. print("Messaggio ricevuto")
  323. #try:
  324. msg = notification["status"]["content"]
  325. id = str(notification["account"]["id"])
  326. username = notification["account"]["acct"]
  327. #except KeyError:
  328. # return
  329. msg = msg.replace("<br>", "\n")
  330. msg = re.compile(r'<.*?>').sub('', msg)
  331. urls = re.findall('http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@.&+]|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+', msg)
  332. user = "m_" + "-".join([i for i in [id, username] if i])
  333. #print (urls)
  334. #print (user)
  335. #visibility = notification['status']['visibility']
  336. statusid = notification['status']['id']
  337. if not urls:
  338. self.mastodon.status_post("@" + username + " " + "Non ho trovato indirizzi validi...", in_reply_to_id = statusid, visibility="direct")
  339. return()
  340. self.mastodon.status_post("@" + username + " " + "Messaggio ricevuto. Elaboro...", in_reply_to_id = statusid, visibility="direct")
  341. for url in urls:
  342. # start the download
  343. dl = addurl(url, user)
  344. self.mastodon.status_post("@" + username + " " + dl, in_reply_to_id = statusid, visibility="direct")
  345. def mastodon_bot():
  346. print ("Bot avviato")
  347. mastodon = Mastodon(access_token = MASTODON_TOKEN, api_base_url = MASTODON_URL)
  348. listener = MastodonListener(mastodon)
  349. mastodon.stream_user(listener)
  350. def listtot(res = sys.maxsize):
  351. plt = listplaylist()
  352. if plt:
  353. announcepos = 0
  354. else:
  355. announcepos = getlastannounce()
  356. if len(plt) < res:
  357. for x in listfallback():
  358. if announcepos == 0:
  359. if os.path.exists("announce/repeat.mp3"):
  360. plt.append(["announce/repeat.mp3"])
  361. announcepos = ANNOUNCEREPEAT - 1
  362. else:
  363. announcepos = (announcepos - 1)
  364. plt.append(x)
  365. return plt[:res]
  366. else:
  367. return plt
  368. def getlastannounce():
  369. announcepos = ANNOUNCEREPEAT - 1
  370. try:
  371. with open("announce/last","r") as f:
  372. announcepos=int(f.readline().rstrip())
  373. except:
  374. pass
  375. return announcepos
  376. def setlastannounce(announcepos):
  377. with open("announce/last","w") as f:
  378. f.write(str(announcepos))
  379. def consume(song):
  380. if os.path.exists(song):
  381. print ("Consumo la canzone " + song)
  382. if song.split("/")[0] == "playlist":
  383. os.remove(song)
  384. if not [x for x in glob(os.path.dirname(song) + "/*") if not os.path.basename(x) == "last"]:
  385. shutil.rmtree(os.path.dirname(song))
  386. else:
  387. with open(os.path.dirname(song) + "/last", "w") as f:
  388. f.write(time.strftime("%Y%m%d%H%M%S"))
  389. #resetta il contatore announcelast
  390. setlastannounce(0)
  391. elif song.split("/")[0] == "fallback":
  392. fname = time.strftime("%Y%m%d%H%M%S") + "|" + "|".join(os.path.basename(song).split("|")[1:])
  393. fname = os.path.dirname(song) + "/" + fname
  394. os.rename(song, fname)
  395. announcepos = getlastannounce()
  396. print("Annuncio da " + str(announcepos) + " a " + str(announcepos - 1))
  397. setlastannounce(announcepos - 1)
  398. elif song.split("/")[0] == "announce":
  399. setlastannounce(ANNOUNCEREPEAT)
  400. def addstartannounce():
  401. #aggiunge l'annuncio iniziale
  402. if os.path.exists("announce/start.mp3"):
  403. fileout = "playlist/announce/00000000000000|start|start.mp3"
  404. copyfile("announce/start.mp3", fileout)
  405. def copyfile(source, dest):
  406. if not os.path.exists(dest):
  407. os.makedirs(os.path.dirname(dest), exist_ok=True)
  408. shutil.copy2(source, dest)
  409. def plaympd():
  410. client = MPDClient()
  411. client.timeout = 10 # network timeout in seconds (floats allowed), default: None
  412. client.idletimeout = None # timeout for fetching the result of the idle command is handled seperately, default: None
  413. client.connect("localhost", 6600) # connect to localhost:6600
  414. #print(client.mpd_version)
  415. looptime = 10
  416. synctime = 5
  417. listlen = 10
  418. if client.status()['state'] != "play":
  419. if SHUFFLEUSERS:
  420. shuffleusers()
  421. if SHUFFLEFALLBACK:
  422. shufflefallback()
  423. #stoppa e svuota
  424. client.stop()
  425. client.clear()
  426. #cancella la cartella mpd
  427. if os.path.exists("mpd"):
  428. shutil.rmtree("mpd")
  429. os.makedirs("mpd")
  430. client.update()
  431. while 'updating_db' in client.status():
  432. pass
  433. #aggiunge l'annuncio iniziale
  434. #addstartannounce()
  435. #resetta il lastannounce
  436. setlastannounce(ANNOUNCEREPEAT - 1)
  437. #riempe la playlist
  438. plt = listtot(listlen)
  439. for f in plt:
  440. print(f[0])
  441. copyfile(f[0], "mpd/" + f[0])
  442. client.update()
  443. while 'updating_db' in client.status():
  444. pass
  445. for f in plt:
  446. client.add(f[0])
  447. #consuma il primo e fa play
  448. consume(plt[0][0])
  449. #mpdadd(client, listlen)
  450. client.play(0)
  451. while True:
  452. # print("Current")
  453. # print(client.currentsong())
  454. # print()
  455. # print("Status")
  456. # print(client.status())
  457. # print()
  458. # print("playlist")
  459. # print(client.playlistinfo())
  460. # print()
  461. #controlla se il pezzo e' il primo e consuma le precedenti
  462. status = client.status()
  463. if int(status['song']) > 0:
  464. #consuma la canzone attuale
  465. song = client.playlistinfo()[int(status['song'])]['file']
  466. consume(song)
  467. mpdclean(client)
  468. # if len(client.playlistinfo()) < listlen:
  469. # mpdsync(client, listlen)
  470. # status = client.status()
  471. #controlla se mancano meno di 15 secondi
  472. #timeleft = float(status['duration']) - float(status['elapsed']) #new mpd
  473. timeleft = float(client.currentsong()['time']) - float(status['elapsed']) #old mpd
  474. if timeleft <= looptime + synctime:
  475. time.sleep(max(timeleft - synctime, 0))
  476. print ("Mancano %d secondi" % (synctime))
  477. mpdclean(client)
  478. mpdadd(client, listlen)
  479. time.sleep(looptime)
  480. def mpdclean(client):
  481. #cancella le precedenti
  482. for x in range(int(client.status()['song'])):
  483. song = client.playlistinfo()[0]['file']
  484. consume(song)
  485. client.delete(0)
  486. #e pulisce anche in mpd
  487. if os.path.exists("mpd/" + song):
  488. os.remove("mpd/" + song)
  489. #se non ci sono + file cancella la cartella
  490. if not glob(os.path.dirname("mpd/" + song) + "/*"):
  491. shutil.rmtree(os.path.dirname("mpd/" + song))
  492. def mpdadd(client, listlen):
  493. print("Rigenero la playlist")
  494. plt = listtot(listlen)
  495. #copia i file
  496. for f in plt:
  497. #print(f[0])
  498. copyfile(f[0], "mpd/" + f[0])
  499. client.update()
  500. while 'updating_db' in client.status():
  501. pass
  502. # #cancella tutto tranne la prima
  503. # for x in client.playlistinfo()[1:]:
  504. # client.delete(1)
  505. # time.sleep(0.5)
  506. # #e rifa la playlist
  507. # for f in plt:
  508. # client.add(f[0])
  509. # time.sleep(0.5)
  510. print("------------------")
  511. playlist=client.playlistinfo()
  512. for f in plt[:len(playlist)-1]:
  513. i = plt.index(f) + 1
  514. #print(f[0] +" - "+ playlist[i]['file'])
  515. if f[0] != playlist[i]['file']:
  516. i = i - 1
  517. break
  518. else:
  519. print("Mantengo " + f[0])
  520. i = i + 1
  521. #print (i)
  522. for x in client.playlistinfo()[i:]:
  523. print("Cancello " + x['file'])
  524. client.delete(i)
  525. #e rifa la playlist
  526. for f in plt[i-1:]:
  527. print("Aggiungo " + f[0])
  528. client.add(f[0])
  529. #consumo la prima
  530. consume(plt[0][0])
  531. if __name__ == '__main__':
  532. print ("This is a package, use other commands to run it")