| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221 |
- 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()
|