olinox 4 rokov pred
rodič
commit
3b36917f95

+ 7 - 8
core/file_utilities.py

@@ -1,20 +1,19 @@
 import hashlib
 import mimetypes
-import os
-import platform
 import subprocess
 import sys
 
-MEDIA_EXTS_CACHE = ['.mp3', '.wma', '.flac', '.mp4']
+MEDIA_EXTS_CACHE = []
+# MEDIA_EXTS_CACHE = ['.mp3', '.wma', '.flac', '.mp4']
 
 
 def media_exts():
     """ List of media file extensions from local system mimetypes """
-    # if not MEDIA_EXTS_CACHE:
-    #     mimetypes.init()
-    #     for ext in mimetypes.types_map:
-    #         if mimetypes.types_map[ext].split('/')[0] in ('audio', 'video'):
-    #             MEDIA_EXTS_CACHE.append(ext)
+    if not MEDIA_EXTS_CACHE:
+        mimetypes.init()
+        for ext in mimetypes.types_map:
+            if mimetypes.types_map[ext].split('/')[0] in ('audio', 'video'):
+                MEDIA_EXTS_CACHE.append(ext)
     return MEDIA_EXTS_CACHE
 
 

+ 60 - 96
core/indexer.py

@@ -1,13 +1,12 @@
 import time
 from collections import deque
-from queue import Queue
-from threading import Thread, Lock
+from threading import Thread, Timer, Event
 
 import vlc
 from PyQt5.QtCore import pyqtSignal, QObject
 from path import Path
 
-from core import db
+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
@@ -17,42 +16,8 @@ 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 AlreadyIndexed(Exception):
+    pass
 
 
 class Emitter(QObject):
@@ -60,103 +25,102 @@ class Emitter(QObject):
     musicFolderStatusChanged = pyqtSignal(int)
 
 
-class Discoverer(CyclicThread):
-    DELAY = 5
+class Indexer(Thread):
+    DELAY = 2
 
-    def __init__(self, indexer):
-        CyclicThread.__init__(self)
-        self.indexer = indexer
+    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)
-        music_folders = music_folder_repo.get_all()
-
         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
+        # Index new files
         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.indexer.emitter.musicFolderStatusChanged.emit(music_folder.id)
+                    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.indexer.emitter.musicFolderStatusChanged.emit(music_folder.id)
+                self.emitter.musicFolderStatusChanged.emit(music_folder.id)
 
+            # walk files
             for filename in music_folder_path.walkfiles():
-                if self.indexer.in_deque(filename):
+                # filename already seen
+                if filename in buffer:
                     continue
+                # new file
                 if filename not in index and is_media_file_ext(filename.ext):
-                    self.indexer.put(filename)
+                    buffer.append(filename)
+                # file already in db
                 elif filename in index:
                     track = index[filename]
-                    if track.status == Track.STATUS_UNAVAILABLE:
-                        self.indexer.put(track.id)
+                    if track.status in (Track.STATUS_UNAVAILABLE, Track.STATUS_UNKNOWN):
+                        buffer.append(track.id)
                     del index[filename]
 
+        # Index missing files
         for filename, track in index.items():
-            if self.indexer.in_deque(track.id):
+            if track.id in buffer:
                 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()
+                buffer.append(track.id)
 
-    def act(self):
-        buffer = []
-        session = db.Session()
-        track_repo = TrackRepository(session)
-
-        for _ in range(self.BUFFER_SIZE):
+        # 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, self.deque.pop())
-                buffer.append(track)
+                track = self.index(track_repo, filename_or_id)
+                tracks.append(track)
+            except AlreadyIndexed:
+                pass
             except (FileNotFoundError, NotSupportedFile) as e:
                 logger.warning("Error during indexation: %s" % e)
                 continue
             except IndexError:
                 break
 
-        if buffer:
-            for track in buffer:
+        # Finalize
+        if tracks:
+            for track in tracks:
                 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
+            self.emitter.filesIndexed.emit(tracks)
+            logger.info(f"{len(tracks)} tracks indexed")
 
     @staticmethod
     def index(track_repo, filename_or_track_id):
@@ -183,10 +147,11 @@ class Indexer(CyclicThread):
             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()
-        track_infos = vlc_media.get_tracks_info()
 
         title = vlc_media.get_meta(vlc.Meta.Title)
         if not title or title == '(null)':
@@ -196,9 +161,7 @@ class Indexer(CyclicThread):
         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.duration = vlc_media.get_duration() // 1000
         track.note = ""
         track.status = Track.STATUS_FOUND
         track.path = filename
@@ -207,8 +170,9 @@ class Indexer(CyclicThread):
         return track
 
     def stop(self):
-        self.discoverer.stop()
-        super().stop()
+        self.stopped.set()
+        while self.is_alive():
+            time.sleep(0.1)
         logger.info('** indexation thread stopped **')
 
 

+ 0 - 2
core/models.py

@@ -49,9 +49,7 @@ class Track(Model):
     artist = Column(String)
     album = Column(String)
     track_num = Column(Integer)
-    year = Column(Integer)
     duration = Column(Integer)
-    size = Column(Integer)
     note = Column(String)
     status = Column(Integer, nullable=False, default=0)
     path = Column(String, nullable=False)

+ 1 - 1
core/repositories.py

@@ -76,7 +76,7 @@ class TrackRepository(Repository):
         return self.query().order_by(Track.artist).order_by(Track.album).order_by(Track.track_num).all()
 
     def get_by_hash(self, hash_):
-        return self.query().filter(hash == hash_).first()
+        return self.query().filter(Track.hash == hash_).first()
 
     def get_by_tag_id(self, tag_id):
         return self.session.query(Track)\

+ 0 - 230
core/vlc_.py

@@ -1,230 +0,0 @@
-#
-# PyQt5 example for VLC Python bindings
-# Copyright (C) 2009-2010 the VideoLAN team
-#
-# This program is free software; you can redistribute it and/or modify
-# it under the terms of the GNU General Public License as published by
-# the Free Software Foundation; either version 2 of the License, or
-# (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program; if not, write to the Free Software
-# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston MA 02110-1301, USA.
-#
-"""
-A simple example for VLC python bindings using PyQt5.
-
-Author: Saveliy Yusufov, Columbia University, sy2685@columbia.edu
-Date: 25 December 2018
-"""
-import mimetypes
-import platform
-import os
-import sys
-
-from PyQt5 import QtWidgets, QtGui, QtCore
-
-from core import constants
-
-os.environ['PYTHON_VLC_LIB_PATH'] = constants.APP_ROOT / 'core' / 'vlc-core' / 'libvlc.dll'
-if 1:
-    import vlc
-
-class Player(QtWidgets.QMainWindow):
-    """A simple Media Player using VLC and Qt
-    """
-
-    def __init__(self, master=None):
-        QtWidgets.QMainWindow.__init__(self, master)
-        self.setWindowTitle("Media Player")
-
-        # Create a basic vlc instance
-        self.instance = vlc.Instance()
-
-        self.media = None
-
-        # Create an empty vlc media player
-        self.mediaplayer = self.instance.media_player_new()
-
-        self.create_ui()
-        self.is_paused = False
-
-    def create_ui(self):
-        """Set up the user interface, signals & slots
-        """
-        self.widget = QtWidgets.QWidget(self)
-        self.setCentralWidget(self.widget)
-
-        # In this widget, the video will be drawn
-        if platform.system() == "Darwin":  # for MacOS
-            self.videoframe = QtWidgets.QMacCocoaViewContainer(0)
-        else:
-            self.videoframe = QtWidgets.QFrame()
-
-        self.palette = self.videoframe.palette()
-        self.palette.setColor(QtGui.QPalette.Window, QtGui.QColor(0, 0, 0))
-        self.videoframe.setPalette(self.palette)
-        self.videoframe.setAutoFillBackground(True)
-
-        self.positionslider = QtWidgets.QSlider(QtCore.Qt.Horizontal, self)
-        self.positionslider.setToolTip("Position")
-        self.positionslider.setMaximum(1000)
-        self.positionslider.sliderMoved.connect(self.set_position)
-        self.positionslider.sliderPressed.connect(self.set_position)
-
-        self.hbuttonbox = QtWidgets.QHBoxLayout()
-        self.playbutton = QtWidgets.QPushButton("Play")
-        self.hbuttonbox.addWidget(self.playbutton)
-        self.playbutton.clicked.connect(self.play_pause)
-
-        self.stopbutton = QtWidgets.QPushButton("Stop")
-        self.hbuttonbox.addWidget(self.stopbutton)
-        self.stopbutton.clicked.connect(self.stop)
-
-        self.hbuttonbox.addStretch(1)
-        self.volumeslider = QtWidgets.QSlider(QtCore.Qt.Horizontal, self)
-        self.volumeslider.setMaximum(100)
-        self.volumeslider.setValue(self.mediaplayer.audio_get_volume())
-        self.volumeslider.setToolTip("Volume")
-        self.hbuttonbox.addWidget(self.volumeslider)
-        self.volumeslider.valueChanged.connect(self.set_volume)
-
-        self.vboxlayout = QtWidgets.QVBoxLayout()
-        self.vboxlayout.addWidget(self.videoframe)
-        self.vboxlayout.addWidget(self.positionslider)
-        self.vboxlayout.addLayout(self.hbuttonbox)
-
-        self.widget.setLayout(self.vboxlayout)
-
-        menu_bar = self.menuBar()
-
-        # File menu
-        file_menu = menu_bar.addMenu("File")
-
-        # Add actions to file menu
-        open_action = QtWidgets.QAction("Load Video", self)
-        close_action = QtWidgets.QAction("Close App", self)
-        file_menu.addAction(open_action)
-        file_menu.addAction(close_action)
-
-        open_action.triggered.connect(self.open_file)
-        close_action.triggered.connect(sys.exit)
-
-        self.timer = QtCore.QTimer(self)
-        self.timer.setInterval(100)
-        self.timer.timeout.connect(self.update_ui)
-
-    def play_pause(self):
-        """Toggle play/pause status
-        """
-        if self.mediaplayer.is_playing():
-            self.mediaplayer.pause()
-            self.playbutton.setText("Play")
-            self.is_paused = True
-            self.timer.stop()
-        else:
-            if self.mediaplayer.play() == -1:
-                self.open_file()
-                return
-
-            self.mediaplayer.play()
-            self.playbutton.setText("Pause")
-            self.timer.start()
-            self.is_paused = False
-
-    def stop(self):
-        """Stop player
-        """
-        self.mediaplayer.stop()
-        self.playbutton.setText("Play")
-
-    def open_file(self):
-        """Open a media file in a MediaPlayer
-        """
-
-        dialog_txt = "Choose Media File"
-        filename = QtWidgets.QFileDialog.getOpenFileName(self, dialog_txt, os.path.expanduser('~'))
-        if not filename:
-            return
-
-        # getOpenFileName returns a tuple, so use only the actual file name
-        self.media = self.instance.media_new(filename[0])
-
-        # Put the media in the media player
-        self.mediaplayer.set_media(self.media)
-
-        # Parse the metadata of the file
-        self.media.parse()
-
-        # Set the title of the track as window title
-        self.setWindowTitle(self.media.get_meta(0))
-
-        # The media player has to be 'connected' to the QFrame (otherwise the
-        # video would be displayed in it's own window). This is platform
-        # specific, so we must give the ID of the QFrame (or similar object) to
-        # vlc. Different platforms have different functions for this
-        if platform.system() == "Linux":  # for Linux using the X Server
-            self.mediaplayer.set_xwindow(int(self.videoframe.winId()))
-        elif platform.system() == "Windows":  # for Windows
-            self.mediaplayer.set_hwnd(int(self.videoframe.winId()))
-        elif platform.system() == "Darwin":  # for MacOS
-            self.mediaplayer.set_nsobject(int(self.videoframe.winId()))
-
-        self.play_pause()
-
-    def set_volume(self, volume):
-        """Set the volume
-        """
-        self.mediaplayer.audio_set_volume(volume)
-
-    def set_position(self):
-        """Set the movie position according to the position slider.
-        """
-
-        # The vlc MediaPlayer needs a float value between 0 and 1, Qt uses
-        # integer variables, so you need a factor; the higher the factor, the
-        # more precise are the results (1000 should suffice).
-
-        # Set the media position to where the slider was dragged
-        self.timer.stop()
-        pos = self.positionslider.value()
-        self.mediaplayer.set_position(pos / 1000.0)
-        self.timer.start()
-
-    def update_ui(self):
-        """Updates the user interface"""
-
-        # Set the slider's position to its corresponding media position
-        # Note that the setValue function only takes values of type int,
-        # so we must first convert the corresponding media position.
-        media_pos = int(self.mediaplayer.get_position() * 1000)
-        self.positionslider.setValue(media_pos)
-
-        # No need to call this function if nothing is played
-        if not self.mediaplayer.is_playing():
-            self.timer.stop()
-
-            # After the video finished, the play button stills shows "Pause",
-            # which is not the desired behavior of a media player.
-            # This fixes that "bug".
-            if not self.is_paused:
-                self.stop()
-
-
-def main():
-    """Entry point for our simple vlc player
-    """
-    app = QtWidgets.QApplication(sys.argv)
-    player = Player()
-    player.show()
-    player.resize(640, 480)
-    sys.exit(app.exec_())
-
-
-if __name__ == "__main__":
-    main()

+ 3 - 18
notes

@@ -6,28 +6,14 @@ TODO:
 
 Bugs:
 
-* Voir le pbm avec les mimetypes
 * voir le bug avec is_subdir_of() lorsque c'est une rép réseau
-* gérer pbm de loks à l'indexation, ex:
-      File "E:\dev\mew\core\indexer.py", line 159, in index
-        track_repo.commit()
-      sqlite3.ProgrammingError: SQLite objects created in a thread can only be used in that same thread. The object was created in thread id 4136 and this is thread id 16548.
 
 Priorité 1:
 
-* explorer: combiner les recherches par tag et texte
-* indexer: mettre à jour l'explorer de manière intelligente après l'indexation
-* explorer: afficher le statut des morceaux
-* indexer: clarifier ce qu'il se passe lorsqu'un morceau apparait plusieurs fois dans les répertoires indexés, et
-  que le morceau indéxé est suppr. Est-ce que ses versions jumelles sont bien indéxées à sa place?
-* mettre à jour l'explorer au fur et à mesure de l'indexation
-* permettre l'ajout de tags
-* faire une modale pour l'édition des metadonnées
-* afficher l'activité de l'indexation dans la barre de statut
-* Enregistrer les notes pour un morceau et s'assurer qu'elles ne soient pas perdues
 * maj les tables playlist et explorer quand les metas d'un fichier sont modifiées
-* permettre de reset les metas d'une piste à partir du fichier ou de remplacer directement les metas du fichier
-* vérifier que les metas éditées ne sont pas ecrasées par l'indexeur
+* (?) permettre de reset les metas d'une piste à partir du fichier ou de remplacer directement les metas du fichier
+* permettre de réordonner correctement les musiques
+* permettre un glisser-déposer de l'explorer vers la playlist
 
 Priorité 2:
 
@@ -38,7 +24,6 @@ Priorité 2:
 * faire un prompt premier lancement pour ajouter des dossiers de musique et créer une première séance
 
 
-
 Demandes mathilde:
 
 * Imprimer la séance

+ 155 - 32
ui/qt/main.ui

@@ -7,13 +7,13 @@
     <x>0</x>
     <y>0</y>
     <width>1029</width>
-    <height>783</height>
+    <height>784</height>
    </rect>
   </property>
   <property name="minimumSize">
    <size>
     <width>1029</width>
-    <height>618</height>
+    <height>784</height>
    </size>
   </property>
   <property name="font">
@@ -68,6 +68,11 @@
           <pointsize>12</pointsize>
          </font>
         </property>
+        <property name="styleSheet">
+         <string notr="true">QListWidget::item {
+	margin: 16px 0;
+}</string>
+        </property>
         <property name="horizontalScrollBarPolicy">
          <enum>Qt::ScrollBarAlwaysOff</enum>
         </property>
@@ -89,12 +94,6 @@
           <height>24</height>
          </size>
         </property>
-        <property name="gridSize">
-         <size>
-          <width>0</width>
-          <height>36</height>
-         </size>
-        </property>
         <property name="currentRow">
          <number>-1</number>
         </property>
@@ -465,7 +464,7 @@
                </layout>
               </item>
               <item>
-               <layout class="QVBoxLayout" name="verticalLayout_5" stretch="0,0,0,0">
+               <layout class="QVBoxLayout" name="verticalLayout_5" stretch="0,0,0">
                 <property name="leftMargin">
                  <number>10</number>
                 </property>
@@ -1131,13 +1130,13 @@
                         <property name="minimumSize">
                          <size>
                           <width>218</width>
-                          <height>160</height>
+                          <height>120</height>
                          </size>
                         </property>
                         <property name="maximumSize">
                          <size>
                           <width>16777215</width>
-                          <height>160</height>
+                          <height>120</height>
                          </size>
                         </property>
                         <property name="cursor" stdset="0">
@@ -1171,7 +1170,13 @@
                   <property name="minimumSize">
                    <size>
                     <width>220</width>
-                    <height>0</height>
+                    <height>38</height>
+                   </size>
+                  </property>
+                  <property name="maximumSize">
+                   <size>
+                    <width>16777215</width>
+                    <height>38</height>
                    </size>
                   </property>
                   <property name="text">
@@ -1197,35 +1202,21 @@
                   <property name="minimumSize">
                    <size>
                     <width>220</width>
-                    <height>0</height>
+                    <height>38</height>
                    </size>
                   </property>
-                  <property name="text">
-                   <string>  Ajouter à la playlist</string>
-                  </property>
-                  <property name="icon">
-                   <iconset resource="rsc.qrc">
-                    <normaloff>:/img/rsc/double-right-arrows-symbol.png</normaloff>:/img/rsc/double-right-arrows-symbol.png</iconset>
-                  </property>
-                 </widget>
-                </item>
-                <item>
-                 <widget class="QPushButton" name="explorerRemoveFromPlaylist">
-                  <property name="enabled">
-                   <bool>false</bool>
-                  </property>
-                  <property name="minimumSize">
+                  <property name="maximumSize">
                    <size>
-                    <width>220</width>
-                    <height>0</height>
+                    <width>16777215</width>
+                    <height>38</height>
                    </size>
                   </property>
                   <property name="text">
-                   <string>  Retirer de la playlist</string>
+                   <string>  Ajouter à la playlist</string>
                   </property>
                   <property name="icon">
                    <iconset resource="rsc.qrc">
-                    <normaloff>:/img/rsc/double-left-arrows-symbol.png</normaloff>:/img/rsc/double-left-arrows-symbol.png</iconset>
+                    <normaloff>:/img/rsc/plus.png</normaloff>:/img/rsc/plus.png</iconset>
                   </property>
                  </widget>
                 </item>
@@ -1354,6 +1345,138 @@
                   </column>
                  </widget>
                 </item>
+                <item>
+                 <layout class="QHBoxLayout" name="horizontalLayout_22">
+                  <item>
+                   <widget class="QToolButton" name="btnPlaylistMoveToTop">
+                    <property name="minimumSize">
+                     <size>
+                      <width>28</width>
+                      <height>28</height>
+                     </size>
+                    </property>
+                    <property name="maximumSize">
+                     <size>
+                      <width>28</width>
+                      <height>28</height>
+                     </size>
+                    </property>
+                    <property name="text">
+                     <string>...</string>
+                    </property>
+                    <property name="icon">
+                     <iconset resource="rsc.qrc">
+                      <normaloff>:/img/rsc/double-up-arrow.png</normaloff>:/img/rsc/double-up-arrow.png</iconset>
+                    </property>
+                   </widget>
+                  </item>
+                  <item>
+                   <widget class="QToolButton" name="btnPlaylistMoveUp">
+                    <property name="minimumSize">
+                     <size>
+                      <width>28</width>
+                      <height>28</height>
+                     </size>
+                    </property>
+                    <property name="maximumSize">
+                     <size>
+                      <width>28</width>
+                      <height>28</height>
+                     </size>
+                    </property>
+                    <property name="text">
+                     <string>...</string>
+                    </property>
+                    <property name="icon">
+                     <iconset resource="rsc.qrc">
+                      <normaloff>:/img/rsc/arrow-up.png</normaloff>:/img/rsc/arrow-up.png</iconset>
+                    </property>
+                   </widget>
+                  </item>
+                  <item>
+                   <widget class="QToolButton" name="btnPlaylistMoveToBelow">
+                    <property name="minimumSize">
+                     <size>
+                      <width>28</width>
+                      <height>28</height>
+                     </size>
+                    </property>
+                    <property name="maximumSize">
+                     <size>
+                      <width>28</width>
+                      <height>288</height>
+                     </size>
+                    </property>
+                    <property name="text">
+                     <string>...</string>
+                    </property>
+                    <property name="icon">
+                     <iconset resource="rsc.qrc">
+                      <normaloff>:/img/rsc/arrow-down.png</normaloff>:/img/rsc/arrow-down.png</iconset>
+                    </property>
+                   </widget>
+                  </item>
+                  <item>
+                   <widget class="QToolButton" name="btnPlaylistMoveToBottom">
+                    <property name="minimumSize">
+                     <size>
+                      <width>28</width>
+                      <height>28</height>
+                     </size>
+                    </property>
+                    <property name="maximumSize">
+                     <size>
+                      <width>28</width>
+                      <height>28</height>
+                     </size>
+                    </property>
+                    <property name="text">
+                     <string>...</string>
+                    </property>
+                    <property name="icon">
+                     <iconset resource="rsc.qrc">
+                      <normaloff>:/img/rsc/double-below-arrow.png</normaloff>:/img/rsc/double-below-arrow.png</iconset>
+                    </property>
+                   </widget>
+                  </item>
+                  <item>
+                   <spacer name="horizontalSpacer_7">
+                    <property name="orientation">
+                     <enum>Qt::Horizontal</enum>
+                    </property>
+                    <property name="sizeHint" stdset="0">
+                     <size>
+                      <width>40</width>
+                      <height>20</height>
+                     </size>
+                    </property>
+                   </spacer>
+                  </item>
+                  <item>
+                   <widget class="QToolButton" name="btnPlaylistRemove">
+                    <property name="minimumSize">
+                     <size>
+                      <width>28</width>
+                      <height>28</height>
+                     </size>
+                    </property>
+                    <property name="maximumSize">
+                     <size>
+                      <width>28</width>
+                      <height>28</height>
+                     </size>
+                    </property>
+                    <property name="text">
+                     <string>...</string>
+                    </property>
+                    <property name="icon">
+                     <iconset resource="rsc.qrc">
+                      <normaloff>:/img/rsc/trash.png</normaloff>:/img/rsc/trash.png</iconset>
+                    </property>
+                   </widget>
+                  </item>
+                 </layout>
+                </item>
                </layout>
               </item>
              </layout>

+ 74 - 31
ui/qt/main_ui.py

@@ -14,8 +14,8 @@ from PyQt5 import QtCore, QtGui, QtWidgets
 class Ui_mainWindow(object):
     def setupUi(self, mainWindow):
         mainWindow.setObjectName("mainWindow")
-        mainWindow.resize(1029, 783)
-        mainWindow.setMinimumSize(QtCore.QSize(1029, 618))
+        mainWindow.resize(1029, 784)
+        mainWindow.setMinimumSize(QtCore.QSize(1029, 784))
         font = QtGui.QFont()
         font.setFamily("Verdana")
         font.setPointSize(8)
@@ -39,13 +39,15 @@ class Ui_mainWindow(object):
         font.setFamily("Verdana")
         font.setPointSize(12)
         self.menu.setFont(font)
+        self.menu.setStyleSheet("QListWidget::item {\n"
+"    margin: 16px 0;\n"
+"}")
         self.menu.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
         self.menu.setEditTriggers(QtWidgets.QAbstractItemView.NoEditTriggers)
         self.menu.setProperty("showDropIndicator", False)
         self.menu.setDefaultDropAction(QtCore.Qt.CopyAction)
         self.menu.setAlternatingRowColors(True)
         self.menu.setIconSize(QtCore.QSize(24, 24))
-        self.menu.setGridSize(QtCore.QSize(0, 36))
         self.menu.setObjectName("menu")
         self.mainLayout.addWidget(self.menu)
         self.verticalLayout_7 = QtWidgets.QVBoxLayout()
@@ -424,8 +426,8 @@ class Ui_mainWindow(object):
         self.label_10.setObjectName("label_10")
         self.verticalLayout_9.addWidget(self.label_10)
         self.explorerTrackNotepad = QtWidgets.QTextEdit(self.page_6)
-        self.explorerTrackNotepad.setMinimumSize(QtCore.QSize(218, 160))
-        self.explorerTrackNotepad.setMaximumSize(QtCore.QSize(16777215, 160))
+        self.explorerTrackNotepad.setMinimumSize(QtCore.QSize(218, 120))
+        self.explorerTrackNotepad.setMaximumSize(QtCore.QSize(16777215, 120))
         self.explorerTrackNotepad.viewport().setProperty("cursor", QtGui.QCursor(QtCore.Qt.IBeamCursor))
         self.explorerTrackNotepad.setAutoFormatting(QtWidgets.QTextEdit.AutoAll)
         self.explorerTrackNotepad.setUndoRedoEnabled(True)
@@ -437,7 +439,8 @@ class Ui_mainWindow(object):
         self.verticalLayout_5.addWidget(self.explorerTrackMetaStack)
         self.explorerTrackPlay = QtWidgets.QPushButton(self.page_3)
         self.explorerTrackPlay.setEnabled(False)
-        self.explorerTrackPlay.setMinimumSize(QtCore.QSize(220, 0))
+        self.explorerTrackPlay.setMinimumSize(QtCore.QSize(220, 38))
+        self.explorerTrackPlay.setMaximumSize(QtCore.QSize(16777215, 38))
         icon5 = QtGui.QIcon()
         icon5.addPixmap(QtGui.QPixmap(":/img/rsc/play.png"), QtGui.QIcon.Normal, QtGui.QIcon.Off)
         self.explorerTrackPlay.setIcon(icon5)
@@ -446,20 +449,13 @@ class Ui_mainWindow(object):
         self.verticalLayout_5.addWidget(self.explorerTrackPlay)
         self.explorerAddToPlaylist = QtWidgets.QPushButton(self.page_3)
         self.explorerAddToPlaylist.setEnabled(False)
-        self.explorerAddToPlaylist.setMinimumSize(QtCore.QSize(220, 0))
+        self.explorerAddToPlaylist.setMinimumSize(QtCore.QSize(220, 38))
+        self.explorerAddToPlaylist.setMaximumSize(QtCore.QSize(16777215, 38))
         icon6 = QtGui.QIcon()
-        icon6.addPixmap(QtGui.QPixmap(":/img/rsc/double-right-arrows-symbol.png"), QtGui.QIcon.Normal, QtGui.QIcon.Off)
+        icon6.addPixmap(QtGui.QPixmap(":/img/rsc/plus.png"), QtGui.QIcon.Normal, QtGui.QIcon.Off)
         self.explorerAddToPlaylist.setIcon(icon6)
         self.explorerAddToPlaylist.setObjectName("explorerAddToPlaylist")
         self.verticalLayout_5.addWidget(self.explorerAddToPlaylist)
-        self.explorerRemoveFromPlaylist = QtWidgets.QPushButton(self.page_3)
-        self.explorerRemoveFromPlaylist.setEnabled(False)
-        self.explorerRemoveFromPlaylist.setMinimumSize(QtCore.QSize(220, 0))
-        icon7 = QtGui.QIcon()
-        icon7.addPixmap(QtGui.QPixmap(":/img/rsc/double-left-arrows-symbol.png"), QtGui.QIcon.Normal, QtGui.QIcon.Off)
-        self.explorerRemoveFromPlaylist.setIcon(icon7)
-        self.explorerRemoveFromPlaylist.setObjectName("explorerRemoveFromPlaylist")
-        self.verticalLayout_5.addWidget(self.explorerRemoveFromPlaylist)
         self.horizontalLayout_6.addLayout(self.verticalLayout_5)
         self.verticalLayout_12 = QtWidgets.QVBoxLayout()
         self.verticalLayout_12.setObjectName("verticalLayout_12")
@@ -509,6 +505,51 @@ class Ui_mainWindow(object):
         self.explorerPlaylist.horizontalHeader().setStretchLastSection(True)
         self.explorerPlaylist.verticalHeader().setVisible(True)
         self.verticalLayout_12.addWidget(self.explorerPlaylist)
+        self.horizontalLayout_22 = QtWidgets.QHBoxLayout()
+        self.horizontalLayout_22.setObjectName("horizontalLayout_22")
+        self.btnPlaylistMoveToTop = QtWidgets.QToolButton(self.page_3)
+        self.btnPlaylistMoveToTop.setMinimumSize(QtCore.QSize(28, 28))
+        self.btnPlaylistMoveToTop.setMaximumSize(QtCore.QSize(28, 28))
+        icon7 = QtGui.QIcon()
+        icon7.addPixmap(QtGui.QPixmap(":/img/rsc/double-up-arrow.png"), QtGui.QIcon.Normal, QtGui.QIcon.Off)
+        self.btnPlaylistMoveToTop.setIcon(icon7)
+        self.btnPlaylistMoveToTop.setObjectName("btnPlaylistMoveToTop")
+        self.horizontalLayout_22.addWidget(self.btnPlaylistMoveToTop)
+        self.btnPlaylistMoveUp = QtWidgets.QToolButton(self.page_3)
+        self.btnPlaylistMoveUp.setMinimumSize(QtCore.QSize(28, 28))
+        self.btnPlaylistMoveUp.setMaximumSize(QtCore.QSize(28, 28))
+        icon8 = QtGui.QIcon()
+        icon8.addPixmap(QtGui.QPixmap(":/img/rsc/arrow-up.png"), QtGui.QIcon.Normal, QtGui.QIcon.Off)
+        self.btnPlaylistMoveUp.setIcon(icon8)
+        self.btnPlaylistMoveUp.setObjectName("btnPlaylistMoveUp")
+        self.horizontalLayout_22.addWidget(self.btnPlaylistMoveUp)
+        self.btnPlaylistMoveToBelow = QtWidgets.QToolButton(self.page_3)
+        self.btnPlaylistMoveToBelow.setMinimumSize(QtCore.QSize(28, 28))
+        self.btnPlaylistMoveToBelow.setMaximumSize(QtCore.QSize(28, 288))
+        icon9 = QtGui.QIcon()
+        icon9.addPixmap(QtGui.QPixmap(":/img/rsc/arrow-down.png"), QtGui.QIcon.Normal, QtGui.QIcon.Off)
+        self.btnPlaylistMoveToBelow.setIcon(icon9)
+        self.btnPlaylistMoveToBelow.setObjectName("btnPlaylistMoveToBelow")
+        self.horizontalLayout_22.addWidget(self.btnPlaylistMoveToBelow)
+        self.btnPlaylistMoveToBottom = QtWidgets.QToolButton(self.page_3)
+        self.btnPlaylistMoveToBottom.setMinimumSize(QtCore.QSize(28, 28))
+        self.btnPlaylistMoveToBottom.setMaximumSize(QtCore.QSize(28, 28))
+        icon10 = QtGui.QIcon()
+        icon10.addPixmap(QtGui.QPixmap(":/img/rsc/double-below-arrow.png"), QtGui.QIcon.Normal, QtGui.QIcon.Off)
+        self.btnPlaylistMoveToBottom.setIcon(icon10)
+        self.btnPlaylistMoveToBottom.setObjectName("btnPlaylistMoveToBottom")
+        self.horizontalLayout_22.addWidget(self.btnPlaylistMoveToBottom)
+        spacerItem4 = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum)
+        self.horizontalLayout_22.addItem(spacerItem4)
+        self.btnPlaylistRemove = QtWidgets.QToolButton(self.page_3)
+        self.btnPlaylistRemove.setMinimumSize(QtCore.QSize(28, 28))
+        self.btnPlaylistRemove.setMaximumSize(QtCore.QSize(28, 28))
+        icon11 = QtGui.QIcon()
+        icon11.addPixmap(QtGui.QPixmap(":/img/rsc/trash.png"), QtGui.QIcon.Normal, QtGui.QIcon.Off)
+        self.btnPlaylistRemove.setIcon(icon11)
+        self.btnPlaylistRemove.setObjectName("btnPlaylistRemove")
+        self.horizontalLayout_22.addWidget(self.btnPlaylistRemove)
+        self.verticalLayout_12.addLayout(self.horizontalLayout_22)
         self.horizontalLayout_6.addLayout(self.verticalLayout_12)
         self.horizontalLayout_6.setStretch(0, 1)
         self.horizontalLayout_6.setStretch(1, 1)
@@ -573,18 +614,16 @@ class Ui_mainWindow(object):
         self.horizontalLayout_7.setObjectName("horizontalLayout_7")
         self.musicFoldersRemoveButton = QtWidgets.QPushButton(self.page_5)
         self.musicFoldersRemoveButton.setMinimumSize(QtCore.QSize(0, 32))
-        icon8 = QtGui.QIcon()
-        icon8.addPixmap(QtGui.QPixmap(":/img/rsc/delete.png"), QtGui.QIcon.Normal, QtGui.QIcon.Off)
-        self.musicFoldersRemoveButton.setIcon(icon8)
+        icon12 = QtGui.QIcon()
+        icon12.addPixmap(QtGui.QPixmap(":/img/rsc/delete.png"), QtGui.QIcon.Normal, QtGui.QIcon.Off)
+        self.musicFoldersRemoveButton.setIcon(icon12)
         self.musicFoldersRemoveButton.setObjectName("musicFoldersRemoveButton")
         self.horizontalLayout_7.addWidget(self.musicFoldersRemoveButton)
-        spacerItem4 = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum)
-        self.horizontalLayout_7.addItem(spacerItem4)
+        spacerItem5 = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum)
+        self.horizontalLayout_7.addItem(spacerItem5)
         self.musicFoldersAddButton = QtWidgets.QPushButton(self.page_5)
         self.musicFoldersAddButton.setMinimumSize(QtCore.QSize(128, 32))
-        icon9 = QtGui.QIcon()
-        icon9.addPixmap(QtGui.QPixmap(":/img/rsc/plus.png"), QtGui.QIcon.Normal, QtGui.QIcon.Off)
-        self.musicFoldersAddButton.setIcon(icon9)
+        self.musicFoldersAddButton.setIcon(icon6)
         self.musicFoldersAddButton.setObjectName("musicFoldersAddButton")
         self.horizontalLayout_7.addWidget(self.musicFoldersAddButton)
         self.verticalLayout_6.addLayout(self.horizontalLayout_7)
@@ -628,20 +667,20 @@ class Ui_mainWindow(object):
         self.tableTagsRemoveButton = QtWidgets.QPushButton(self.page_5)
         self.tableTagsRemoveButton.setMinimumSize(QtCore.QSize(100, 32))
         self.tableTagsRemoveButton.setMaximumSize(QtCore.QSize(100, 32))
-        self.tableTagsRemoveButton.setIcon(icon8)
+        self.tableTagsRemoveButton.setIcon(icon12)
         self.tableTagsRemoveButton.setObjectName("tableTagsRemoveButton")
         self.horizontalLayout_20.addWidget(self.tableTagsRemoveButton)
-        spacerItem5 = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum)
-        self.horizontalLayout_20.addItem(spacerItem5)
+        spacerItem6 = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum)
+        self.horizontalLayout_20.addItem(spacerItem6)
         self.tagsTableAddButton = QtWidgets.QPushButton(self.page_5)
         self.tagsTableAddButton.setMinimumSize(QtCore.QSize(128, 32))
         self.tagsTableAddButton.setMaximumSize(QtCore.QSize(128, 32))
-        self.tagsTableAddButton.setIcon(icon9)
+        self.tagsTableAddButton.setIcon(icon6)
         self.tagsTableAddButton.setObjectName("tagsTableAddButton")
         self.horizontalLayout_20.addWidget(self.tagsTableAddButton)
         self.verticalLayout_6.addLayout(self.horizontalLayout_20)
-        spacerItem6 = QtWidgets.QSpacerItem(20, 40, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding)
-        self.verticalLayout_6.addItem(spacerItem6)
+        spacerItem7 = QtWidgets.QSpacerItem(20, 40, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding)
+        self.verticalLayout_6.addItem(spacerItem7)
         self.horizontalLayout_5.addLayout(self.verticalLayout_6)
         self.stack.addWidget(self.page_5)
         self.verticalLayout_7.addWidget(self.stack)
@@ -705,7 +744,6 @@ class Ui_mainWindow(object):
         self.explorerTrackNotepad.setPlaceholderText(_translate("mainWindow", "Mes notes sur ce morceau..."))
         self.explorerTrackPlay.setText(_translate("mainWindow", "  Lire le morceau sélectionné"))
         self.explorerAddToPlaylist.setText(_translate("mainWindow", "  Ajouter à la playlist"))
-        self.explorerRemoveFromPlaylist.setText(_translate("mainWindow", "  Retirer de la playlist"))
         self.label_9.setText(_translate("mainWindow", "Ma séance: "))
         self.explorerLblPlaylistTitle.setText(_translate("mainWindow", "(pas de séance en cours)"))
         self.btnSelectPlaylist.setText(_translate("mainWindow", "..."))
@@ -715,6 +753,11 @@ class Ui_mainWindow(object):
         item.setText(_translate("mainWindow", "track_id"))
         item = self.explorerPlaylist.horizontalHeaderItem(2)
         item.setText(_translate("mainWindow", "Label"))
+        self.btnPlaylistMoveToTop.setText(_translate("mainWindow", "..."))
+        self.btnPlaylistMoveUp.setText(_translate("mainWindow", "..."))
+        self.btnPlaylistMoveToBelow.setText(_translate("mainWindow", "..."))
+        self.btnPlaylistMoveToBottom.setText(_translate("mainWindow", "..."))
+        self.btnPlaylistRemove.setText(_translate("mainWindow", "..."))
         self.label.setText(_translate("mainWindow", "Page 4"))
         self.label_5.setText(_translate("mainWindow", "Mes dossiers de musique"))
         item = self.settingsMusicFoldersTable.horizontalHeaderItem(0)

+ 5 - 0
ui/qt/rsc.qrc

@@ -1,5 +1,10 @@
 <RCC>
   <qresource prefix="/img">
+    <file>rsc/trash.png</file>
+    <file>rsc/double-below-arrow.png</file>
+    <file>rsc/double-up-arrow.png</file>
+    <file>rsc/arrow-down.png</file>
+    <file>rsc/arrow-up.png</file>
     <file>rsc/unknown.png</file>
     <file>rsc/valid.png</file>
     <file>rsc/invalid.png</file>

BIN
ui/qt/rsc/arrow-down.png


BIN
ui/qt/rsc/arrow-up.png


BIN
ui/qt/rsc/double-below-arrow.png


BIN
ui/qt/rsc/double-up-arrow.png


BIN
ui/qt/rsc/trash.png


Rozdielové dáta súboru neboli zobrazené, pretože súbor je príliš veľký
+ 755 - 1645
ui/qt/rsc_rc.py


+ 28 - 27
ui/qt/widgets/explorertable.py

@@ -20,43 +20,53 @@ class ExplorerTable(QTreeWidget):
         self.setColumnWidth(0, 54)
         self.hideColumn(2)
         self.itemDoubleClicked.connect(self._itemDoubleClicked)
+        self.artist_items, self.album_items, self.track_items = {}, {}, {}
 
     def populate(self):
         session = db.Session()
         track_repo = TrackRepository(session)
         self.clear()
 
-        artist_folders = {}
-        album_folders = {}
+        self.artist_items, self.album_items, self.track_items = {}, {}, {}
+
         tracks = track_repo.get_all()
 
         self.setUpdatesEnabled(False)
+        self.update_with(tracks)
+        self.setUpdatesEnabled(True)
+        self.trackSelected.emit(None)
+
+    def update_with(self, tracks):
 
         for track in tracks:
-            if track.artist and track.artist not in artist_folders:
+            if track.artist and track.artist not in self.artist_items:
                 item = QTreeWidgetItem(self)
                 item.setIcon(0, QIcon(":/img/rsc/artist.png"))
                 item.setText(1, track.artist)
-                artist_folders[track.artist] = item
+                self.artist_items[track.artist] = item
 
         for track in tracks:
             # list albums with artists
-            if track.artist and track.album and (track.artist, track.album) not in album_folders:
-                item = QTreeWidgetItem(artist_folders[track.artist])
+            if track.artist and track.album and (track.artist, track.album) not in self.album_items:
+                item = QTreeWidgetItem(self.artist_items[track.artist])
                 item.setIcon(0, QIcon(":/img/rsc/album.png"))
                 item.setText(1, track.album)
-                album_folders[(track.artist, track.album)] = item
+                self.album_items[(track.artist, track.album)] = item
 
         for track in tracks:
-            if track.artist and track.album:
-                parent = album_folders[(track.artist, track.album)]
-            elif track.artist:
-                parent = artist_folders[track.artist]
-            else:
-                parent = self
 
-            item = QTreeWidgetItem(parent)
-            item.setIcon(0, QIcon(":/img/rsc/music.png"))
+            if track.id not in self.track_items:
+                if track.artist and track.album:
+                    parent = self.album_items[(track.artist, track.album)]
+                elif track.artist:
+                    parent = self.artist_items[track.artist]
+                else:
+                    parent = self
+
+                item = QTreeWidgetItem(parent)
+                item.setIcon(0, QIcon(":/img/rsc/music.png"))
+                item.setData(2, 0, track.id)
+                self.track_items[track.id] = item
 
             if track.status != track.STATUS_UNAVAILABLE:
                 title = track.title
@@ -65,18 +75,9 @@ class ExplorerTable(QTreeWidget):
                 title = f"[Introuvable] {track.title}"
                 color = QColor(200, 20, 20)
 
-            item.setText(1, title)
-            item.setForeground(0, QBrush(color))
-            item.setForeground(1, QBrush(color))
-            item.setData(2, 0, track.id)
-
-        self.setUpdatesEnabled(True)
-        self.trackSelected.emit(None)
-
-    def update_tracks(self, tracks):
-        # for track in tracks:
-        #     pass
-        self.populate()
+            self.track_items[track.id].setText(1, title)
+            self.track_items[track.id].setForeground(0, QBrush(color))
+            self.track_items[track.id].setForeground(1, QBrush(color))
 
     def selected_track(self):
         track_id = self.selectionModel().selection().indexes()[2].data(2)

+ 2 - 1
ui/window.py

@@ -5,6 +5,7 @@
     @author:[author], [year]
 """
 from PyQt5 import QtWidgets
+from PyQt5.QtCore import QSize
 from path import Path
 
 from PyQt5.QtGui import QIcon
@@ -139,7 +140,7 @@ class MainWindow(QMainWindow):
 
     def refresh_explorer_tree(self, tracks=None):
         if tracks:
-            self.ui.explorerTable.update_tracks(tracks)
+            self.ui.explorerTable.update_with(tracks)
         else:
             self.ui.explorerTable.populate()
         self.ui.explorerLineSearch.clear()

Niektoré súbory nie sú zobrazené, pretože je v týchto rozdielových dátach zmenené mnoho súborov