indexer.py 5.8 KB

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