playlistalo.py 19 KB

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