indexer.py 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181
  1. import time
  2. from collections import deque
  3. from threading import Thread, Timer, Event
  4. import vlc
  5. from PyQt5.QtCore import pyqtSignal, QObject
  6. from path import Path
  7. from core import db
  8. from core.exceptions import NotSupportedFile
  9. from core.file_utilities import is_media_file_ext, hash_file
  10. from core.logging_ import Logger
  11. from core.models import Track
  12. from core.repositories import MusicFolderRepository, TrackRepository
  13. logger = Logger.get()
  14. class AlreadyIndexed(Exception):
  15. pass
  16. class Emitter(QObject):
  17. filesIndexed = pyqtSignal(list)
  18. musicFolderStatusChanged = pyqtSignal(int)
  19. class Indexer(Thread):
  20. DELAY = 2
  21. def __init__(self):
  22. Thread.__init__(self)
  23. self.stopped = Event()
  24. self.emitter = Emitter()
  25. self.timer = Timer(self.DELAY, self.act)
  26. def run(self):
  27. logger.info('** indexation thread started **')
  28. while not self.stopped.wait(self.DELAY):
  29. self.act()
  30. def act(self):
  31. # Initialize
  32. session = db.Session()
  33. music_folder_repo = MusicFolderRepository(session)
  34. track_repo = TrackRepository(session)
  35. # Get current data
  36. music_folders = music_folder_repo.get_all()
  37. tracks = track_repo.get_all()
  38. # Index existing
  39. index = {t.path: t for t in tracks}
  40. buffer = deque()
  41. # -- Walk through music folders
  42. # Index new files
  43. for music_folder in music_folders:
  44. music_folder_path = Path(music_folder.path)
  45. if not music_folder_path.exists():
  46. if music_folder.status == music_folder.STATUS_FOUND:
  47. music_folder.status = music_folder.STATUS_UNAVAILABLE
  48. music_folder_repo.commit()
  49. self.emitter.musicFolderStatusChanged.emit(music_folder.id)
  50. continue
  51. if music_folder.status != music_folder.STATUS_FOUND:
  52. music_folder.status = music_folder.STATUS_FOUND
  53. music_folder_repo.commit()
  54. self.emitter.musicFolderStatusChanged.emit(music_folder.id)
  55. for filename in music_folder_path.walkfiles():
  56. if filename in buffer:
  57. continue
  58. if filename not in index and is_media_file_ext(filename.ext):
  59. buffer.append(filename)
  60. elif filename in index:
  61. track = index[filename]
  62. if track.status == Track.STATUS_UNAVAILABLE:
  63. buffer.append(track.id)
  64. del index[filename]
  65. # Index missing files
  66. for filename, track in index.items():
  67. if track.id in buffer:
  68. continue
  69. filename = Path(filename)
  70. if not filename.exists() and track.status != Track.STATUS_UNAVAILABLE:
  71. buffer.append(track.id)
  72. # Index buffered tracks
  73. tracks = []
  74. while buffer:
  75. filename_or_id = buffer.pop()
  76. try:
  77. track = self.index(track_repo, filename_or_id)
  78. tracks.append(track)
  79. except AlreadyIndexed:
  80. pass
  81. except (FileNotFoundError, NotSupportedFile) as e:
  82. logger.warning("Error during indexation: %s" % e)
  83. continue
  84. except IndexError:
  85. break
  86. # Finalize
  87. if tracks:
  88. for track in tracks:
  89. if track.id is None:
  90. track_repo.create(track)
  91. track_repo.commit()
  92. self.emitter.filesIndexed.emit(tracks)
  93. logger.info(f"{len(tracks)} tracks indexed")
  94. @staticmethod
  95. def index(track_repo, filename_or_track_id):
  96. """ index a media file from the filesystem or a track id """
  97. if type(filename_or_track_id) is int:
  98. track = track_repo.get_by_id(filename_or_track_id)
  99. filename = Path(track.path)
  100. track_hash = track.hash
  101. if not filename.exists() and track.status != Track.STATUS_UNAVAILABLE:
  102. logger.debug('Index - missing: %s' % filename)
  103. track.status = Track.STATUS_UNAVAILABLE
  104. return track
  105. else:
  106. filename = Path(filename_or_track_id)
  107. if not filename.exists():
  108. raise FileNotFoundError(f"File not found: {filename}")
  109. if not is_media_file_ext(filename.ext):
  110. raise NotSupportedFile(f"File's extension {filename.ext} is not supported")
  111. track_hash = hash_file(filename)
  112. track = track_repo.get_by_hash(track_hash)
  113. if not track:
  114. track = Track()
  115. elif track.status == Track.STATUS_FOUND:
  116. raise AlreadyIndexed(f"File already indexed")
  117. vlc_media = vlc.Media(filename)
  118. vlc_media.parse()
  119. track_infos = vlc_media.get_tracks_info()
  120. title = vlc_media.get_meta(vlc.Meta.Title)
  121. if not title or title == '(null)':
  122. title = filename.stripext().name
  123. track.title = title
  124. track.format = filename.ext
  125. track.artist = vlc_media.get_meta(vlc.Meta.AlbumArtist) or vlc_media.get_meta(vlc.Meta.Artist)
  126. track.album = vlc_media.get_meta(vlc.Meta.Album)
  127. track.track_num = vlc_media.get_meta(vlc.Meta.TrackNumber)
  128. # track.year = vlc_media.get_meta(vlc.Meta.Date)
  129. # track.duration = vlc_media.get_meta(vlc.Meta.Date)
  130. # track.size = 0
  131. track.note = ""
  132. track.status = Track.STATUS_FOUND
  133. track.path = filename
  134. track.hash = track_hash
  135. return track
  136. def stop(self):
  137. self.stopped.set()
  138. while self.is_alive():
  139. time.sleep(0.1)
  140. logger.info('** indexation thread stopped **')
  141. if __name__ == '__main__':
  142. indexer = Indexer()
  143. indexer.start()
  144. try:
  145. indexer.join()
  146. except KeyboardInterrupt:
  147. indexer.stop()