playlistalo.py 17 KB

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