import time from collections import deque from queue import Queue from threading import Thread, Lock import vlc from PyQt5.QtCore import pyqtSignal, QObject from path import Path from core import db from core.exceptions import NotSupportedFile from core.file_utilities import is_media_file_ext, hash_file from core.logging_ import Logger from core.models import Track from core.repositories import MusicFolderRepository, TrackRepository logger = Logger.get() class CyclicThread(Thread): DELAY = 0 def __init__(self): Thread.__init__(self) self.interrupted = False self.last_exec = 0 self.running = False def act(self): raise NotImplementedError() def run(self): t = None self.running = True try: while 1: if self.DELAY: t = time.time() if not self.DELAY or not self.last_exec or (t - self.last_exec) > self.DELAY: self.act() self.last_exec = t if self.interrupted: break time.sleep(0.1) finally: self.running = False def trigger(self): self.last_exec = 0 def stop(self): self.interrupted = True class Emitter(QObject): filesIndexed = pyqtSignal(list) musicFolderStatusChanged = pyqtSignal(int) class Discoverer(CyclicThread): DELAY = 5 def __init__(self, indexer): CyclicThread.__init__(self) self.indexer = indexer def act(self): session = db.Session() music_folder_repo = MusicFolderRepository(session) music_folders = music_folder_repo.get_all() track_repo = TrackRepository(session) tracks = track_repo.get_all() index = {t.path: t for t in tracks} for music_folder in music_folders: music_folder_path = Path(music_folder.path) if not music_folder_path.exists(): if music_folder.status == music_folder.STATUS_FOUND: music_folder.status = music_folder.STATUS_UNAVAILABLE music_folder_repo.commit() self.indexer.emitter.musicFolderStatusChanged.emit(music_folder.id) continue if music_folder.status != music_folder.STATUS_FOUND: music_folder.status = music_folder.STATUS_FOUND music_folder_repo.commit() self.indexer.emitter.musicFolderStatusChanged.emit(music_folder.id) for filename in music_folder_path.walkfiles(): if self.indexer.in_deque(filename): continue if filename not in index and is_media_file_ext(filename.ext): self.indexer.put(filename) elif filename in index: track = index[filename] if track.status == Track.STATUS_UNAVAILABLE: self.indexer.put(track.id) del index[filename] for filename, track in index.items(): if self.indexer.in_deque(track.id): continue filename = Path(filename) if not filename.exists() and track.status != Track.STATUS_UNAVAILABLE: self.indexer.put(track.id) class Indexer(CyclicThread): DELAY = 2 BUFFER_SIZE = 100 def __init__(self): CyclicThread.__init__(self) self.deque = deque() self.interrupted = False self.discoverer = Discoverer(self) self.last_commit = None self.tracks = [] self.emitter = Emitter() def start(self): logger.info('** indexation thread started **') self.discoverer.start() super().start() def act(self): buffer = [] session = db.Session() track_repo = TrackRepository(session) for _ in range(self.BUFFER_SIZE): try: track = self.index(track_repo, self.deque.pop()) buffer.append(track) except (FileNotFoundError, NotSupportedFile) as e: logger.warning("Error during indexation: %s" % e) continue except IndexError: break if buffer: for track in buffer: if track.id is None: track_repo.create(track) track_repo.commit() self.emitter.filesIndexed.emit(buffer) logger.info(f"{len(buffer)} tracks indexed") def put(self, filename_or_track_id): self.deque.appendleft(filename_or_track_id) def in_deque(self, filename_or_track_id): return filename_or_track_id in self.deque @staticmethod def index(track_repo, filename_or_track_id): """ index a media file from the filesystem or a track id """ if type(filename_or_track_id) is int: track = track_repo.get_by_id(filename_or_track_id) filename = Path(track.path) track_hash = track.hash if not filename.exists() and track.status != Track.STATUS_UNAVAILABLE: logger.debug('Index - missing: %s' % filename) track.status = Track.STATUS_UNAVAILABLE return track else: filename = Path(filename_or_track_id) if not filename.exists(): raise FileNotFoundError(f"File not found: {filename}") if not is_media_file_ext(filename.ext): raise NotSupportedFile(f"File's extension {filename.ext} is not supported") track_hash = hash_file(filename) track = track_repo.get_by_hash(track_hash) if not track: track = Track() vlc_media = vlc.Media(filename) vlc_media.parse() track_infos = vlc_media.get_tracks_info() title = vlc_media.get_meta(vlc.Meta.Title) if not title or title == '(null)': title = filename.stripext().name track.title = title track.format = filename.ext track.artist = vlc_media.get_meta(vlc.Meta.AlbumArtist) or vlc_media.get_meta(vlc.Meta.Artist) track.album = vlc_media.get_meta(vlc.Meta.Album) track.track_num = vlc_media.get_meta(vlc.Meta.TrackNumber) # track.year = vlc_media.get_meta(vlc.Meta.Date) # track.duration = vlc_media.get_meta(vlc.Meta.Date) # track.size = 0 track.note = "" track.status = Track.STATUS_FOUND track.path = filename track.hash = track_hash return track def stop(self): self.discoverer.stop() super().stop() logger.info('** indexation thread stopped **') if __name__ == '__main__': indexer = Indexer() indexer.start() try: indexer.join() except KeyboardInterrupt: indexer.stop()