#coding:utf-8 ''' Pierre Cadart Script pour NAS sous Linux Utilise un accès en FTP ''' import posixpath import re import config import piexel import tokenizer import filerule from ftplib import FTP import time import file def ftpwalk(directory, ftp): """ Parcours en profondeur un dossier du serveur FTP Effectue un parcours préfixe, semblable à la fonction os.walk """ to_visit = [directory] while len(to_visit) > 0: current = to_visit.pop(0) ftp.cwd(current) Lfiles = [] Ldirs = [] for name, prop in ftp.mlsd(): if name.startswith('.'): continue if prop['type'] == 'dir': Ldirs.append(name) to_visit.append(current+'/'+name) elif prop['type'] == 'file': Lfiles.append(name) # ne construit pas la liste complète, # mais retourne les résultats intermédiaires yield (current, Ldirs, Lfiles) def visit_folder(domain, api, rules, tok): """ Visite un dossier sur un serveur, et ajoute les fichiers trouvés à l'API """ # Connection au serveur print('connect to:', domain['server']) ftp = FTP(domain['server'][6:], user=domain['username'], passwd=domain['password']) ftp.encoding = 'UTF-8' # Initialisation des listes de mises à jour L_missing = [] # fichiers non trouvés sur le serveur FTP L_unreferenced = [] # fichiers non référencés dans l'API L_moved = [] # fichiers déplacés sur le serveur FTP # Lecture des fichiers sur le serveur FTP Lloc = [] for path, _, files in ftpwalk(domain['path'], ftp): # Ajoute les fichiers correspondants aux extensions for f in files: match = filerule.match_rules(path+'/'+f, rules) if match: #print('got match:',match[1], 'name:',path+'/'+f) F = file.File(path, f, match[1]) F.extract_title(tok) Lloc.append(F) ftp.close() print('total loc for ',domain['server']+domain['path'],':', len(Lloc)) # Récupère les fichiers de l'api Lapi = [] for info in api.get_files(path=domain['server']+domain['path'], like=1, filable=1): nfo = {} if ('filable' not in info) or ('filable_type' not in info): print('nfo:', info) # le fileable associé a été supprimé else: year = int(info['filable']['release_date'][:4]) nfo['YEAR'] = year F = file.File(info['path'][len(domain['server']):], info['name'], nfo, api_id=info['filable_id'], api_fileid=info['id'], api_fileable_type=info['filable_type']) F.extract_title(tok) match = filerule.match_rules(F.path+'/'+F.name, rules) if match: F.info = match[1] F.info['YEAR'] = nfo['YEAR'] # may not work :-( Lapi.append(F) print('total api for ',domain['server']+domain['path'],':', len(Lapi)) # traite les films Lfilm_loc = [f for f in Lloc if f.fileable_type == 'film'] Lfilm_api = [f for f in Lapi if f.fileable_type == 'film'] handle_films(Lfilm_loc, Lfilm_api, domain, api, rules, tok) # traite les épisodes Lepisode_loc = [f for f in Lloc if f.fileable_type == 'episode'] Lepisode_api = [f for f in Lapi if f.fileable_type == 'episode'] handle_episodes(Lepisode_loc, Lepisode_api, domain, api, rules, tok) print('visit finished ',domain['server']+domain['path']) def handle_films(Lfilm_loc, Lfilm_api, domain, api, rules, tok): Lloc = Lfilm_loc Lapi = Lfilm_api #print('loc titles:', '|'.join(sorted([f.title for f in Lloc]))) #print('loc titles:', '|'.join(sorted([f.title for f in Lloc]))) #print('\n'*2) #print('api titles:', '|'.join(sorted([f.title for f in Lapi]))) # supprime les dossiers de l'api (ils ne devraient pas apparaître) Linvalid = [f for f in Lapi if (not tok.conf.is_valid_file(f.name)) or not (filerule.match_rules(f.path+'/'+f.name, rules))] Lapi = [f for f in Lapi if tok.conf.is_valid_file(f.name)] # Compare avec la liste de l'api Lmissing = [f for f in Lapi if f not in Lloc] # fichiers non présents localement Lunref = [f for f in Lloc if f not in Lapi] # fichiers non référencés # Fichiers déplacés (ou copiés) localement Lrelink = [] # liste des références à changer for file2 in Lunref: for file1 in Lapi: if file1.filename_same(file2): Lrelink.append((file1, file2)) break for fApi, fLoc in Lrelink: if fApi in Lmissing: Lmissing.remove(fApi) Lunref.remove(fLoc) print('moved/copied:', Lrelink) # Linke les fichiers identiques au même film Llink = [] for file1 in Lunref: for file2 in Lapi: if file1.title == file2.title: Llink.append((file1, file2)) print('D add:', file1, file2, file2.api_id) break print('doubles:', sorted(Llink, key=lambda f:str(f))) for f, fApi in Llink: if fApi in Lmissing: Lmissing.remove(fApi) Lunref.remove(f) # Linke les films par nom si possible APIfilms = api.get_films() API_alltitles = [] for f in APIfilms: if f['title']: t = f['title'].replace(' ','').lower() if len(t) <= 2: if t not in [e[0] for e in API_alltitles]: API_alltitles.append((t, f['id'])) if f['title_vo']: t = f['title_vo'].replace(' ','').lower() if len(t) <= 2: if t not in [e[0] for e in API_alltitles]: API_alltitles.append((t, f['id'])) Llink2 = [] for film in Lunref: for title, fid in API_alltitles: if title==film.title: Llink2.append((film, fid)) break # pour ne pas référencer deux fois le même fichier #print(film, ' <-> ', [f for f in APIfilms if f['id']==fid][0]['title']) print('easy ref:', sorted(Llink2, key=lambda f:str(f))) for f, _ in Llink2: Lunref.remove(f) print('invalid:'+'\n'.join(str(f.api_fileid)+' '+str(f) for f in Linvalid)) print('missing (', len(Lmissing), '):','\n'.join([str(e.api_id)+':'+repr(e)+'('+e.title+')' for e in sorted(Lmissing, key=lambda e:e.title)])) print('unreferenced:'+'\n'.join(str(f) for f in sorted(Lunref, key=lambda e:e.title))) #print('unreferenced titles:\n', '\n'.join(sorted([f.title for f in Lunref]))) return # Supprime les fichiers invalides (dossiers/ ne répondent à aucune règle) for i, film in enumerate(Linvalid): print('['+str(i+1)+'/'+str(len(Linvalid))+']'+'invalid:', film) try: resp = api.delete_file(id=film.api_fileid) except Exception as e: print(e) print('film '+film.title+' not deleted') raise Exception('end') time.sleep(1) # Supprime les fichiers qui n'existent plus for i, film in enumerate(Lmissing): print('['+str(i+1)+'/'+str(len(Lmissing))+']'+'missing:', film) try: resp = api.delete_file(id=film.api_fileid) except Exception as e: print(e) print('film '+film.title+' not deleted') raise Exception('end') time.sleep(1) # Put les renommages / déplacements i = 0 for filmApi, filmLoc in Lrelink: i += 1 print('['+str(i)+'/'+str(len(Lrelink))+']'+'relink:', filmApi.title) try: api.put_file(id=filmApi.api_fileid, path=domain['server']+filmLoc.path, name=filmLoc.name) time.sleep(1) except Exception as e: print(e) print('film '+filmApi.title+' not edited') raise Exception('end') # Poste les ajouts de doubles i = 0 for film, filmAPI in Llink: filmID = filmAPI.api_id i += 1 print('['+str(i)+'/'+str(len(Llink))+']'+'link:', film.title) try: resp = api.post_file(path=domain['server']+film.path, name=film.name, type='film', type_id=filmID) if 'id' in resp: post_markers(api, film, resp['id']) time.sleep(1) except Exception as e: print(e) print('film '+film.title+' not added') raise Exception('end') # Poste les ajouts de doubles plus complexes i = 0 for film, filmID in Llink2: i += 1 print('['+str(i)+'/'+str(len(Llink2))+']'+'link2:', film.title) try: resp = api.post_file(path=domain['server']+film.path, name=film.name, type='film', type_id=filmID, **film.additional_info()) if 'id' in resp: post_markers(api, film, resp['id']) time.sleep(1) except Exception as e: print(e) print('film '+film.title+' not added') raise Exception('end') # Poste tout les films locaux (doit faire une reqête Tmdb, qui peut ne pas marcher) i = 0 Lcannot_post = [] for film in Lunref: i += 1 print('['+str(i)+'/'+str(len(Lunref))+']'+'post:', film.title, str(film.info.get('YEAR'))) try: posted = False if 'YEAR' in film.info: # tente avec l'année spécifié en premier resp = api.post_film(title=film.title, year=film.info['YEAR']) if "id" in resp: # id du film posted = True if not posted: resp = api.post_film(title=film.title) if "id" in resp: # id du film posted = True if posted: print('post: path=',domain['server']+film.path) resp = api.post_file(path=domain['server']+film.path, name=film.name, type='film', type_id=resp["id"]) if 'id' in resp: # id du file post_markers(api, film, resp['id']) else: Lcannot_post.append(film) print('response:', resp) time.sleep(1) except Exception as e: print(e) print('film '+film.title+' not posted') raise Exception('end') # TODO: traiter les films non postés (Lcannot_post) def handle_episodes(Lepisode_loc, Lepisode_api, domain, api, rules, tok): Lloc = Lepisode_loc Lapi = Lepisode_api # fichiers invalides Linvalid = [f for f in Lapi if (not tok.conf.is_valid_file(f.name)) or not (filerule.match_rules(f.path+'/'+f.name, rules))] Lapi = [f for f in Lapi if tok.conf.is_valid_file(f.name)] # Compare avec la liste de l'api Lmissing = [f for f in Lapi if f not in Lloc] # fichiers non présents localement Lunref = [f for f in Lloc if f not in Lapi] # fichiers non référencés # de même avec les noms de séries Lseries_loc = list(set([f.info['SERIE'] for f in Lloc])) Lseries_api = list(set([f.info['SERIE'] for f in Lapi])) Lunref_serie = [s for s in Lseries_loc if s not in Lseries_api] # récupère les séries, et les correspondances des noms locaux APIseries = api.get_series(episodes=1) APIepisodes = api.get_episodes(files=1) APIepisodes_byid = {e['id']:e for e in APIepisodes} APIfiles_byid = {f.api_id:f for f in Lapi} series_id_bytitle = {} series_id_bysimpletitle = {} for s in APIseries: series_id_bysimpletitle[s['title'].lower()] = s['id'] for e_id in [e['id'] for e in s['episodes']]: e = APIepisodes_byid[e_id] if 'files' in e: for f in e['files']: serie_title = APIfiles_byid[f['id']].info['SERIE'] series_id_bytitle[serie_title] = s['id'] # tente de lier les séries par nom simplifié for s in Lseries_loc: if s.lower() in series_id_bysimpletitle: series_id_bytitle[s] = series_id_bysimpletitle[s.lower()] if s in Lunref_serie: Lunref_serie.remove(s) # Supprime les fichiers invalides (dossiers/ ne répondent à aucune règle) for i, episode in enumerate(Linvalid): print('['+str(i+1)+'/'+str(len(Linvalid))+']'+'invalid:', episode) try: resp = api.delete_file(id=episode.api_fileid) except Exception as e: print(e) print('episode '+episode.title+' not deleted') raise Exception('end') time.sleep(1) # Supprime les fichiers qui n'existent plus for i, episode in enumerate(Lmissing): print('['+str(i+1)+'/'+str(len(Lmissing))+']'+'missing:', episode) try: resp = api.delete_file(id=episode.api_fileid) except Exception as e: print(e) print('episode '+episode.title+' not deleted') raise Exception('end') time.sleep(1) # Poste les séries non présentes, récupère les références correspondantes series_not_found = [] for i, serie in enumerate(Lunref_serie): print('['+str(i+1)+'/'+str(len(Lunref_serie))+']'+'unref_serie:', serie) try: resp = api.post_serie(title=serie) if 'id' in resp: series_id_bytitle[serie] = resp['id'] print('response: ', resp) else: series_not_found.append(serie) print('not found: ', resp) except Exception as e: print(e) print('serie '+serie+' not posted') raise Exception('end') time.sleep(1) # filtre les épisodes, enlève les séries non référencées Lunref = [f for f in Lunref if f.info['SERIE'] in series_id_bytitle] # Poste les episodes locaux for i, episode in enumerate(Lunref): print('['+str(i+1)+'/'+str(len(Lunref))+']'+'post:', episode) try: serie_id = series_id_bytitle[episode.info['SERIE']] episode_num = int(episode.info['EPISODE_NUM']) season_num = int(episode.info['SEASON_NUM']) resp = api.post_episode(serie_id=serie_id, episode=episode_num, season=season_num) if 'id' in resp: resp = api.post_file(path=domain['server']+episode.path, name=episode.name, type='episode', type_id=resp["id"]) print('response: ',resp) else: print('episode not posted:', resp) except Exception as e: print(e) print('episode '+episode.title+' not posted') #raise Exception('end') time.sleep(1) # TODO: traiter les séries non référencées (series_not_found) def post_markers(api, file_, fileid): for lang in file_.markers['lang']: api.post_file_language(fileid, lang) time.sleep(0.5) for qual in file_.markers['quality']: api.post_file_qualities(fileid, qual) time.sleep(0.5) for sub in file_.markers['subtitle']: api.post_file_subtitle(fileid, sub) time.sleep(0.5) def main(): conf = config.Config() api = piexel.Piexel(conf.server, conf.app, conf.token) tokens = tokenizer.Tokenizer(conf, api) folders = api.get_folders() rules = api.get_paths() for fold in folders[1:]: applicable = [filerule.FileRule(re.escape(fold['path'])+'\\/'+r['regex'], conf) for r in rules if int(r['indexer_folder_id']) == fold['id']] visit_folder(fold, api, applicable, tokens) if __name__ == '__main__': main()