import time from collections import deque from threading import Thread, Timer, Event from PyQt5.QtCore import pyqtSignal, QObject from path import Path from core import db, file_utilities 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 from core.vlc_ import vlc logger = Logger.get() class AlreadyIndexed(Exception): pass class Emitter(QObject): filesIndexed = pyqtSignal(list) musicFolderStatusChanged = pyqtSignal(int) class Indexer(Thread): DELAY = 2 def __init__(self): Thread.__init__(self) self.stopped = Event() self.emitter = Emitter() self.timer = Timer(self.DELAY, self.act) def run(self): logger.info('** indexation thread started **') while not self.stopped.wait(self.DELAY): # logger.debug("... indexation") self.act() def act(self): # Initialize session = db.Session() music_folder_repo = MusicFolderRepository(session) track_repo = TrackRepository(session) # Get current data music_folders = music_folder_repo.get_all() tracks = track_repo.get_all() # Index existing index = {t.path: t for t in tracks} buffer = deque() # -- Walk through music folders # Put new files in buffer for music_folder in music_folders: music_folder_path = Path(music_folder.path) # music folder cant be found 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.emitter.musicFolderStatusChanged.emit(music_folder.id) continue # music folder found if music_folder.status != music_folder.STATUS_FOUND: music_folder.status = music_folder.STATUS_FOUND music_folder_repo.commit() self.emitter.musicFolderStatusChanged.emit(music_folder.id) # walk files for filename in music_folder_path.walkfiles(): # filename already seen if filename in buffer: continue # new file if filename not in index and is_media_file_ext(filename.ext): buffer.append(filename) # file already in db elif filename in index: track = index[filename] if track.status in (Track.STATUS_UNAVAILABLE, Track.STATUS_UNKNOWN): buffer.append(track.id) del index[filename] # Put missing files in buffer for filename, track in index.items(): if track.id in buffer: continue filename = Path(filename) if not filename.exists() and track.status != Track.STATUS_UNAVAILABLE: buffer.append(track.id) # Index buffered tracks # NB: the tracks are treated from the end to the beginning, so missing files are treated before the new ones tracks = [] while buffer: filename_or_id = buffer.pop() try: track = self.index(track_repo, filename_or_id, tracks) tracks.append(track) except AlreadyIndexed: pass except (FileNotFoundError, NotSupportedFile) as e: logger.warning("Error during indexation: %s" % e) continue except IndexError: break # Finalize if tracks: for track in tracks: if track.id is None: track_repo.create(track) track_repo.commit() self.emitter.filesIndexed.emit(tracks) logger.info(f"{len(tracks)} tracks indexed") @staticmethod def index(track_repo, filename_or_track_id, previously_indexed=None): """ index a media file from the filesystem or a track id """ previously_indexed = previously_indexed or [] 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) if any(t.hash == track_hash for t in previously_indexed): raise AlreadyIndexed(f"File already indexed") track = track_repo.get_by_hash(track_hash) if not track: track = Track() elif track.status == Track.STATUS_FOUND: raise AlreadyIndexed(f"File already indexed") vlc_media = vlc.Media(filename) vlc_media.parse() title = vlc_media.get_meta(vlc.Meta.Title) if not title or title == '(null)' or title == filename.name: title = filename.stripext().name track.title = title.strip() track.format = filename.ext track.artist = (vlc_media.get_meta(vlc.Meta.AlbumArtist) or vlc_media.get_meta(vlc.Meta.Artist)).strip() track.album = vlc_media.get_meta(vlc.Meta.Album).strip() track.track_num = vlc_media.get_meta(vlc.Meta.TrackNumber) track.duration = vlc_media.get_duration() // 1000 track.note = "" track.status = Track.STATUS_FOUND track.path = filename track.hash = track_hash return track def stop(self): self.stopped.set() while self.is_alive(): time.sleep(0.1) logger.info('** indexation thread stopped **') if __name__ == '__main__': indexer = Indexer() indexer.start() try: indexer.join() except KeyboardInterrupt: indexer.stop()