playlistalo.py 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587
  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 len(plt) < res:
  290. announcepos = getlastannounce()
  291. for x in listfallback():
  292. if announcepos == 0:
  293. if os.path.exists("announce/repeat.mp3"):
  294. plt.append(["announce/repeat.mp3"])
  295. announcepos = ANNOUNCEREPEAT - 1
  296. else:
  297. announcepos = (announcepos - 1)
  298. plt.append(x)
  299. return plt[:res]
  300. def getlastannounce():
  301. announcepos = ANNOUNCEREPEAT
  302. try:
  303. with open("announce/last","r") as f:
  304. announcepos=int(f.readline().rstrip())
  305. except:
  306. pass
  307. return announcepos
  308. def setlastannounce(announcepos):
  309. with open("announce/last","w") as f:
  310. f.write(str(announcepos))
  311. def consume(song):
  312. if os.path.exists(song):
  313. print ("consumo la canzone " + song)
  314. if song.split("/")[0] == "playlist":
  315. os.remove(song)
  316. if not [x for x in glob(os.path.dirname(song) + "/*") if not os.path.basename(x) == "last"]:
  317. shutil.rmtree(os.path.dirname(song))
  318. else:
  319. with open(os.path.dirname(song) + "/last", "w") as f:
  320. f.write(time.strftime("%Y%m%d%H%M%S"))
  321. #resetta il contatore announcelast
  322. setlastannounce(ANNOUNCEREPEAT)
  323. elif song.split("/")[0] == "fallback":
  324. fname = time.strftime("%Y%m%d%H%M%S") + "|" + "|".join(os.path.basename(song).split("|")[1:])
  325. fname = os.path.dirname(song) + "/" + fname
  326. os.rename(song, fname)
  327. announcepos = getlastannounce()
  328. print("Annuncio da " + str(announcepos) + " a " + str(announcepos - 1))
  329. setlastannounce(announcepos - 1)
  330. elif song.split("/")[0] == "announce":
  331. setlastannounce(ANNOUNCEREPEAT)
  332. def addstartannounce():
  333. #aggiunge l'annuncio iniziale
  334. if os.path.exists("announce/start.mp3"):
  335. fileout = "playlist/announce/00000000000000|start|start.mp3"
  336. copyfile("announce/start.mp3", fileout)
  337. def copyfile(source, dest):
  338. if not os.path.exists(dest):
  339. os.makedirs(os.path.dirname(dest), exist_ok=True)
  340. shutil.copy2(source, dest)
  341. def plaympd():
  342. client = MPDClient()
  343. client.timeout = 10 # network timeout in seconds (floats allowed), default: None
  344. client.idletimeout = None # timeout for fetching the result of the idle command is handled seperately, default: None
  345. client.connect("localhost", 6600) # connect to localhost:6600
  346. #print(client.mpd_version)
  347. looptime = 10
  348. synctime = 5
  349. listlen = 10
  350. if SHUFFLEUSERS:
  351. shuffleusers()
  352. if SHUFFLEFALLBACK:
  353. shufflefallback()
  354. #stoppa e svuota
  355. client.stop()
  356. client.clear()
  357. #cancella la cartella mpd
  358. if os.path.exists("mpd"):
  359. shutil.rmtree("mpd")
  360. os.makedirs("mpd")
  361. client.rescan()
  362. #aggiunge l'annuncio iniziale
  363. addstartannounce()
  364. #resetta il lastannounce
  365. setlastannounce(ANNOUNCEREPEAT)
  366. #riempe la playlist
  367. plt = listtot(listlen)
  368. for f in plt:
  369. print(f[0])
  370. copyfile(f[0], "mpd/" + f[0])
  371. client.rescan()
  372. time.sleep(0.5)
  373. for f in plt:
  374. client.add(f[0])
  375. #consuma il primo e fa play
  376. consume(plt[0][0])
  377. #mpdadd(client, listlen)
  378. client.play(0)
  379. while True:
  380. # print("Current")
  381. # print(client.currentsong())
  382. # print()
  383. # print("Status")
  384. # print(client.status())
  385. # print()
  386. # print("playlist")
  387. # print(client.playlistinfo())
  388. # print()
  389. #controlla se il pezzo e' il primo e consuma le precedenti
  390. status = client.status()
  391. if int(status['song']) > 0:
  392. #consuma la canzone attuale
  393. song = client.playlistinfo()[int(status['song'])]['file']
  394. consume(song)
  395. mpdclean(client)
  396. # if len(client.playlistinfo()) < listlen:
  397. # mpdsync(client, listlen)
  398. # status = client.status()
  399. #controlla se mancano meno di 15 secondi
  400. #timeleft = float(status['duration']) - float(status['elapsed']) #new mpd
  401. timeleft = float(client.currentsong()['time']) - float(status['elapsed']) #old mpd
  402. if timeleft <= looptime + synctime:
  403. time.sleep(max(timeleft - synctime, 0))
  404. print ("Mancano %d secondi" % (synctime))
  405. mpdclean(client)
  406. mpdadd(client, listlen)
  407. time.sleep(looptime)
  408. def mpdclean(client):
  409. #cancella le precedenti
  410. for x in range(int(client.status()['song'])):
  411. song = client.playlistinfo()[0]['file']
  412. consume(song)
  413. client.delete(0)
  414. #e pulisce anche in mpd
  415. #if os.path.exists("mpd/" + song):
  416. # os.remove("mpd/" + song)
  417. #se non ci sono + file cancella la cartella
  418. if not glob(os.path.dirname("mpd/" + song) + "/*"):
  419. shutil.rmtree(os.path.dirname("mpd/" + song))
  420. def mpdadd(client, listlen):
  421. print("Rigenero la playlist")
  422. plt = listtot(listlen)
  423. #copia i file
  424. for f in plt:
  425. #print(f[0])
  426. copyfile(f[0], "mpd/" + f[0])
  427. client.rescan()
  428. time.sleep(0.5)
  429. # # #cancella tutto tranne la prima
  430. # for x in client.playlistinfo()[1:]:
  431. # client.delete(1)
  432. # time.sleep(0.5)
  433. # #e rifa la playlist
  434. # for f in plt:
  435. # client.add(f[0])
  436. # time.sleep(0.5)
  437. print("------------------")
  438. playlist=client.playlistinfo()
  439. for f in plt[:len(playlist)-1]:
  440. i = plt.index(f) + 1
  441. #print(f[0] +" - "+ playlist[i]['file'])
  442. if f[0] != playlist[i]['file']:
  443. break
  444. else:
  445. print("Mantengo " + f[0])
  446. #print (i)
  447. for x in client.playlistinfo()[i:]:
  448. print("cancello " + x['file'])
  449. client.delete(i)
  450. #e rifa la playlist
  451. for f in plt[i-1:]:
  452. print("Aggiungo " + f[0])
  453. client.add(f[0])
  454. #consumo la prima
  455. consume(plt[0][0])
  456. if __name__ == '__main__':
  457. print ("This is a package, use other commands to run it")