Преглед изворни кода

Implement simple repeat mechanism

niels пре 4 година
родитељ
комит
d00e294ca9
6 измењених фајлова са 118 додато и 26 уклоњено
  1. 46 10
      chube.py
  2. 2 0
      chube_enums.py
  3. 2 0
      static/js/enums.js
  4. 62 13
      static/js/main.js
  5. 1 1
      static/js/websocketResolver.js
  6. 5 2
      static/player.html

+ 46 - 10
chube.py

@@ -16,11 +16,15 @@ class Chueue:
     _codes: Dict[int, str]
     _id_iter: Iterator[int]
 
+    _played_queue: Optional[List[int]]
+    _repeat_enabled: bool = False
+
     def __init__(self):
         self._lock = RLock()
         self._queue = []
         self._codes = dict()
         self._id_iter = cycle(range(sys.maxsize))
+        self._played_queue = None
 
     def add(self, code):
         with self:
@@ -44,22 +48,45 @@ class Chueue:
 
     def pop(self):
         with self:
-            if len(self._queue) > 0:
-                song_id = self._queue.pop(0)
+            if len(self._queue) <= 0:
+                if self._repeat_enabled and len(self._played_queue) > 0:
+                    self._queue = self._played_queue
+                    self._played_queue = []
+                else:
+                    return None
+                
+            song_id = self._queue.pop(0)
+            if self._repeat_enabled:
+                code = self._codes[song_id]
+                self._played_queue.append(song_id)
+            else:
                 code = self._codes.pop(song_id)
-                return self.as_song(song_id, code)
+            return self.as_song(song_id, code)
+
+    def set_repeat_enabled(self, enable, playback_song):
+        self._repeat_enabled = enable
+        if enable:
+            if playback_song is not None:
+                self._played_queue = [playback_song["id"]]
+                self._codes[playback_song["id"]] = playback_song["code"]
             else:
-                return None
+                self._played_queue = []
+        else:
+            self._played_queue = None
+
+    def is_repeat_enabled(self):
+        return self._repeat_enabled
 
     def as_song(self, song_id, code=None):
         if code is None:
             code = self._codes[song_id]
         return {"id": song_id, "code": code}
 
-    def as_list(self):
+    def as_lists(self):
         with self:
-            res = list(map(self.as_song, self._queue))
-        return res
+            queue_as_list = list(map(self.as_song, self._queue))
+            played_as_list = list(map(self.as_song, self._played_queue)) if self.is_repeat_enabled() else None
+            return {"next": queue_as_list, "previous": played_as_list}
 
     def lock(self):
         self._lock.acquire()
@@ -128,7 +155,7 @@ rooms: Dict[str, Room] = dict()
 async def request_state_processor(ws, data, path):
     room = rooms[path]
     await ws.send(make_message(Message.STATE, {
-        "list": room.chueue.as_list(),
+        "lists": room.chueue.as_lists(),
         "playing": room.playback.get_song(),
         "state": room.playback.get_state().value
     }))
@@ -144,7 +171,8 @@ async def request_list_operation_processor(ws, data, path):
         if kind == YoutubeResourceType.VIDEO.value:
             code = data["code"]
             song_id = chueue.add(code)
-            message = make_message(Message.LIST_OPERATION, {"op": QueueOp.ADD.value, "items": [{"code": code, "id": song_id}]})
+            message = make_message(Message.LIST_OPERATION,
+                                   {"op": QueueOp.ADD.value, "items": [{"code": code, "id": song_id}]})
         elif kind == YoutubeResourceType.PLAYLIST.value:
             code = data["code"]
             playlist_items = await chube_youtube.get_all_playlist_items(code)
@@ -171,7 +199,8 @@ async def request_list_operation_processor(ws, data, path):
         actual_displacement = chueue.move(song_id, displacement)
         if actual_displacement != 0:
             message = make_message(Message.LIST_OPERATION,
-                                   {"op": QueueOp.MOVE.value, "items": [{"id": song_id, "displacement": actual_displacement}]})
+                                   {"op": QueueOp.MOVE.value,
+                                    "items": [{"id": song_id, "displacement": actual_displacement}]})
 
     if message is not None:
         await room.channel.send(message)
@@ -215,6 +244,13 @@ async def media_action_processor(ws, data, path):
         if send_pause:
             await room.channel.send(make_message(Message.MEDIA_ACTION, {"action": MediaAction.PAUSE.value}))
 
+    if action == MediaAction.REPEAT.value:
+        enable = data["enable"]
+        if room.chueue.is_repeat_enabled() != enable:
+            with room.chueue:
+                room.chueue.set_repeat_enabled(enable, room.playback.get_song())
+            await room.channel.send(make_message(Message.MEDIA_ACTION, {"action": MediaAction.REPEAT.value, "enable": enable}))
+
 
 async def obtain_control_processor(ws, data, path):
     room = rooms[path]

+ 2 - 0
chube_enums.py

@@ -30,6 +30,8 @@ class MediaAction(Enum):
     PAUSE = "PAUSE"
     NEXT = "NEXT"
     PREVIOUS = "PREVIOUS"
+    REPEAT = "REPEAT"
+    SHUFFLE = "SHUFFLE"
 
 
 class PlayerState(Enum):

+ 2 - 0
static/js/enums.js

@@ -22,6 +22,8 @@ export const MediaAction = {
     PAUSE: "PAUSE",
     NEXT: "NEXT",
     PREVIOUS: "PREVIOUS",
+    REPEAT: "REPEAT",
+    SHUFFLE: "SHUFFLE",
 }
 
 

+ 62 - 13
static/js/main.js

@@ -17,12 +17,15 @@ const PLAYER_WIDTH = 640
 const PLAYER_HEIGHT = 360
 
 let videos = null;
+let videosPlayed = null;
 
 let videoPlaying = null;
 let state = null;
 
 let isLeader = null;
 
+let repeat = false;
+
 let player = null;
 let playerActive = false;
 document.getElementById('player').append(mockPlayer('360', '640'));
@@ -96,11 +99,23 @@ function addVideo(code, id) {
 
 function popVideo() {
     console.log("pop", videos)
+    if (videos.length <= 0) {
+        if (videosPlayed) {
+            videos = []
+            for (const { code, id } of videosPlayed) {
+                videos.push({code, id});
+                makeQueueLine(code, id);
+            }
+            videosPlayed = []
+        } else {
+            state = PlayerState.LIST_END
+            return undefined;
+        }
+    }
     const vid = videos.shift();
-    if (vid !== undefined) {
-        queueElement.removeChild(queueElement.querySelector(".videoListCard"));
-    } else {
-        state = PlayerState.LIST_END
+    queueElement.removeChild(queueElement.querySelector(".videoListCard"));
+    if (videosPlayed) {
+        videosPlayed.push(vid);
     }
     return vid;
 }
@@ -370,15 +385,25 @@ function onNextButton(event) {
     }
 }
 
+const repeatButton = document.getElementById('repeat-button');
+
+function onRepeatButton(event) {
+    event.preventDefault();
+    socket.send(makeMessage(MessageTypes.MEDIA_ACTION, { action: MediaAction.REPEAT, enable: !repeat }))
+}
+
 function stateProcessor(ws, data) {
-    const { playing, state: newState, list } = data;
+    const { playing, state: newState, lists } = data;
+    const { next, previous } = lists
 
     videos = []
+    videosPlayed = previous
+    repeat = !!previous
     state = newState
     videoPlaying = playing
 
     const codes = []
-    for (const song of list) {
+    for (const song of next) {
         const { code, id } = song
         addVideo(code, id)
         if (!(codes.includes(code))) {
@@ -386,7 +411,16 @@ function stateProcessor(ws, data) {
         }
     }
 
-    socket.send(makeMessage(MessageTypes.SEARCH_ID, { id: codes }))
+    for (const song of previous || []) {
+        const { code, id } = song;
+        if (!(codes.includes(code))) {
+            codes.push(code)
+        }
+    }
+
+    if (codes.length > 0) {
+        socket.send(makeMessage(MessageTypes.SEARCH_ID, { id: codes }))
+    }
 
     if (videoPlaying !== null) {
         if (state === PlayerState.PLAYING) {
@@ -398,6 +432,10 @@ function stateProcessor(ws, data) {
     if (isLeader === null) {
         setLeader(false)
     }
+    if (repeat) {
+        repeatButton.classList.toggle('btn-outline-secondary');
+        repeatButton.classList.toggle('btn-secondary');
+    }
     afterStateInit()
 }
 
@@ -405,7 +443,7 @@ function listOperationProcessor(ws, data) {
     const { op, items } = data;
     if (op === ListOperationTypes.ADD) {
         const noCodeInfo = []
-        for (const { code, id, snippet } of items){
+        for (const { code, id, snippet } of items) {
             if (snippet !== undefined) {
                 codeInfoMap.set(code, snippet)
             } else if (!codeInfoMap.has(code)) {
@@ -414,10 +452,10 @@ function listOperationProcessor(ws, data) {
             addVideo(code, id);
         }
         if (noCodeInfo.length > 0) {
-            socket.send(makeMessage(MessageTypes.SEARCH_ID, {id: noCodeInfo.join(',')}))
+            socket.send(makeMessage(MessageTypes.SEARCH_ID, { id: noCodeInfo.join(',') }))
         }
     } else if (op === ListOperationTypes.DEL) {
-        for (const {id} of items) {
+        for (const { id } of items) {
             delVideo(id);
         }
     } else if (op === ListOperationTypes.MOVE) {
@@ -428,7 +466,7 @@ function listOperationProcessor(ws, data) {
 }
 
 function mediaActionProcessor(ws, data) {
-    const { action, ended_id, current_id } = data;
+    const { action, ended_id, current_id, enable } = data;
     if (action === MediaAction.PLAY && state === PlayerState.PAUSED) {
         state = PlayerState.PLAYING
         player.playVideo();
@@ -445,6 +483,13 @@ function mediaActionProcessor(ws, data) {
                 state = PlayerState.LIST_END;
             }
         }
+    } else if (action === MediaAction.REPEAT) {
+        if (enable !== repeat) {
+            repeat = enable;
+            repeatButton.classList.toggle('btn-outline-secondary');
+            repeatButton.classList.toggle('btn-secondary');
+            videosPlayed = repeat ? (videoPlaying ? [videoPlaying] : []) : null;
+        }
     }
 }
 
@@ -494,7 +539,6 @@ function makeSearchResult(item) {
     searchResult.setAttribute('data-youtubeID', code)
 
 
-
     function onClickHandler() {
         socket.send(makeMessage(MessageTypes.LIST_OPERATION, {
             op: ListOperationTypes.ADD,
@@ -514,7 +558,11 @@ function makeSearchResult(item) {
     const img = document.createElement('img')
     thumbnailImage.appendChild(img)
 
-    const {url, width, height} = (thumbnails ? thumbnails["default"] : {url: "/img/no_thumbnail.png", width: 120, height: 90})
+    const { url, width, height } = (thumbnails ? thumbnails["default"] : {
+        url: "/img/no_thumbnail.png",
+        width: 120,
+        height: 90
+    })
 
     img.setAttribute('src', url)
     img.setAttribute('width', width)
@@ -586,4 +634,5 @@ function afterStateInit() {
     document.getElementById('play-button').addEventListener('click', onPlayButton)
     document.getElementById('pause-button').addEventListener('click', onPauseButton)
     document.getElementById('next-button').addEventListener('click', onNextButton)
+    document.getElementById('repeat-button').addEventListener('click', onRepeatButton)
 }

+ 1 - 1
static/js/websocketResolver.js

@@ -2,7 +2,7 @@ const PORT = 3821
 const HOST = location.hostname
 const PATH = new URLSearchParams(location.search).get("room")
 if (PATH === "" || PATH === undefined || PATH === null) {
-    location = "/";
+    window.location = "/";
 }
 
 export class Resolver {

+ 5 - 2
static/player.html

@@ -84,16 +84,19 @@
                     <div id="playerContainer" class="py-4" hidden>
                         <div class="row">
                             <div class="btn-toolbar col-11">
-                                <div class="btn btn-group">
+                                <div class="btn-group mr-2">
                                     <button id="leader-button" class="btn btn-outline-secondary disabled">
                                         No connection
                                     </button>
                                 </div>
-                                <div class="btn btn-group">
+                                <div class="btn-group mr-2">
                                     <button id="play-button" class="btn btn-outline-secondary"><i class="fa fa-play"></i></button>
                                     <button id="pause-button" class="btn btn-outline-secondary"><i class="fa fa-pause"></i></button>
                                     <button id="next-button" class="btn btn-outline-secondary"><i class="fa fa-forward"></i></button>
                                 </div>
+                                <div class="btn-group mr-2" title="repeat">
+                                    <button id="repeat-button" class="btn btn-outline-secondary"><i class="fa fa-redo-alt"></i></button>
+                                </div>
                             </div>
                             <span class="col-1">
                                 <a href="#" id="closePlayer">