indexer.py 6.7 KB

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