فهرست منبع

Add thumbnail, title, channel and description to the video list (queue)

Also add a footer with a link to the GitHub
niels 4 سال پیش
والد
کامیت
1f3c566001
7فایلهای تغییر یافته به همراه152 افزوده شده و 23 حذف شده
  1. 1 0
      chube_enums.py
  2. 26 6
      chube_search.py
  3. 54 4
      static/css/main.css
  4. 1 0
      static/index.html
  5. 2 1
      static/js/enums.js
  6. 49 2
      static/js/main.js
  7. 19 10
      static/player.html

+ 1 - 0
chube_enums.py

@@ -14,6 +14,7 @@ class Message(Enum):
     RELEASE_CONTROL = "RELEASE_CONTROL"
     # SEARCH
     SEARCH = "SEARCH"
+    SEARCH_ID = "SEARCH_ID"
 
 
 # List Operations

+ 26 - 6
chube_search.py

@@ -1,34 +1,54 @@
 import asyncio
 import os
+
 import aiohttp
 
 from chube_enums import Message
 from chube_ws import Resolver, make_message
 
 API_KEY = os.environ.get('GOOGLE_SEARCH_API_KEY')
-URL = "https://www.googleapis.com/youtube/v3/search"
+SEARCH_URL = "https://www.googleapis.com/youtube/v3/search"
+SEARCH_ID_URL = "https://www.googleapis.com/youtube/v3/videos"
+
+BASE_QUERY_SEARCH = [('part', 'snippet'), ('type', 'video'), ('videoEmbeddable', 'true'), ('safeSearch', 'none'),
+                     ('key', API_KEY)]
 
-base_query = [('part', 'snippet'), ('type', 'video'), ('videoEmbeddable', 'true'), ('safeSearch', 'none'), ('key', API_KEY)]
+BASE_QUERY_ID_SEARCH = [('part', 'snippet'), ('key', API_KEY)]
 
 
 async def search_processor(ws, data, path):
     async with aiohttp.ClientSession() as session:
-        async with session.get(URL, params=base_query + [('q', data['q'])]) as response:
+        async with session.get(SEARCH_URL, params=BASE_QUERY_SEARCH + [('q', data['q'])]) as response:
             json_data = await response.json()
             await ws.send(make_message(Message.SEARCH, json_data))
-    
+
+
+async def search_id_processor(ws, data, path):
+    ids = data["id"]
+    if not isinstance(ids, list):
+        ids = {ids}
+    else:
+        ids = set(ids)
+    async with aiohttp.ClientSession() as session:
+        async with session.get(SEARCH_ID_URL, params=BASE_QUERY_ID_SEARCH + [('id', ",".join(ids))]) as response:
+            json_data = await response.json()
+            await ws.send(make_message(Message.SEARCH_ID, json_data))
+
 
 def make_resolver():
     resolver = Resolver()
     resolver.register(Message.SEARCH, search_processor)
+    resolver.register(Message.SEARCH_ID, search_id_processor)
     return resolver
 
 
 if __name__ == "__main__":
     async def foo():
         async with aiohttp.ClientSession() as session:
-            async with session.get(URL, params=base_query + [('q', "She")]) as response:
+            async with session.get(SEARCH_URL, params=BASE_QUERY_SEARCH + [('q', "She")]) as response:
                 json_data = await response.json()
                 print(json_data)
+
+
     loop = asyncio.get_event_loop()
-    loop.run_until_complete(foo())
+    loop.run_until_complete(foo())

+ 54 - 4
static/css/main.css

@@ -9,20 +9,23 @@ body {
 
 input, select, textarea, button{font-family:inherit;}
 
-.searchResultChannel {
+.searchResultChannel, .videoListCardChannel {
     font-style: italic;
 }
 
-.searchResultDescription {
+.searchResultDescription, .videoListCardDescription {
     font-size: small;
 }
 
 .searchResult {
-    height: 126px;
     cursor: pointer;
 }
 
-.searchResultTextContainer {
+.searchResult, .videoListCard {
+    height: 126px;
+}
+
+.searchResultTextContainer, .videoListCardTextContainer {
     height: 101px;
     overflow: hidden;
     text-overflow: ellipsis;
@@ -37,3 +40,50 @@ input, select, textarea, button{font-family:inherit;}
 
     margin: auto;
 }
+
+
+.footer {
+    position: fixed;
+    left: 0;
+    bottom: 0;
+    width: 100%;
+    text-align: center;
+}
+
+.footer a {
+    color: gray;
+    font-size: small;
+}
+
+.videoListCardMoveUp {
+    margin-top: 21px;
+    margin-bottom: 11px
+}
+
+.videoListCardMoveUp, .videoListCardMoveDown {
+    text-align: center;
+    font-size: small;
+    width: 25px;
+    height: 25px;
+    border-radius: 50%;
+    border: 1px solid #6c757d;
+    color: #6c757d;
+    cursor: pointer;
+}
+
+.videoListCardMoveUp:hover, .videoListCardMoveDown:hover {
+    background-color: #6c757d;
+    color: white;
+}
+
+.videoListCardMoveUp i, .videoListCardMoveDown i {
+    margin-top: 5px
+}
+
+.videoListCardDelete {
+    margin-left: -15px
+}
+
+.videoListCardThumbnail {
+    margin-top: 5px;
+}

+ 1 - 0
static/index.html

@@ -39,6 +39,7 @@
         </div>
     </div>
 </div>
+<footer class="footer mb-4"><a href="https://github.com/NielsOverkamp/Chu-Chube">Made by Niels Overkamp</a></footer>
 <script src="js/index.js" type="module"></script>
 </body>
 

+ 2 - 1
static/js/enums.js

@@ -5,7 +5,8 @@ export const MessageTypes = {
     SONG_END: "SONG_END",
     OBTAIN_CONTROL: "OBTAIN_CONTROL",
     RELEASE_CONTROL: "RELEASE_CONTROL",
-    SEARCH: "SEARCH"
+    SEARCH: "SEARCH",
+    SEARCH_ID: "SEARCH_ID"
 }
 
 export const ListOperationTypes = {

+ 49 - 2
static/js/main.js

@@ -23,6 +23,8 @@ let isLeader = null;
 let player = null;
 document.getElementById('player').append(mockPlayer('360', '640'));
 
+const codeInfoMap = new Map()
+
 function onYouTubeIframeAPIReady() {
     onYTDone();
 }
@@ -191,8 +193,25 @@ function makeQueueLine(code, id, before_id) {
     let newQueueLine = queueLine.cloneNode(true);
     newQueueLine.id = "";
     newQueueLine.hidden = false;
-    newQueueLine.querySelector('.templateText').innerText = code;
     newQueueLine.setAttribute("data-id", id)
+    if (codeInfoMap.has(code)) {
+        const { id, snippet } = codeInfoMap.get(code)
+        const { thumbnails, title, channelTitle, publishTime, description } = snippet
+
+        const thumbnail = newQueueLine.getElementsByClassName("videoListCardThumbnail")[0]
+        const img = document.createElement('img')
+        thumbnail.appendChild(img)
+        img.setAttribute('src', thumbnails.default.url)
+        img.setAttribute('width', thumbnails.default.width)
+        img.setAttribute('height', thumbnails.default.height)
+        img.setAttribute('alt', "")
+
+        newQueueLine.getElementsByClassName("videoListCardTitle")[0].innerText = title
+        newQueueLine.getElementsByClassName("videoListCardChannel")[0].innerText = channelTitle
+        newQueueLine.getElementsByClassName("videoListCardDescription")[0].innerText = description.replace(/\n/g, " ")
+    }
+
+    newQueueLine.setAttribute("data-youtubeId", code)
 
     function delHandler(event) {
         onDeleteClick(event, id)
@@ -271,11 +290,17 @@ function stateProcessor(ws, data) {
     state = newState
     videoPlaying = playing
 
+    const codes = []
     for (const song of list) {
         const {code, id} = song
         addVideo(code, id)
+        if (!(codes.includes(code))) {
+            codes.push(code)
+        }
     }
 
+    socket.send(makeMessage(MessageTypes.SEARCH_ID, {id: codes}))
+
     if (videoPlaying !== null) {
         if (state === PlayerState.PLAYING) {
             playVideo(videoPlaying)
@@ -294,6 +319,9 @@ function listOperationProcessor(ws, data) {
     if (op === ListOperationTypes.ADD) {
         const { code } = data;
         addVideo(code, id);
+        if (!codeInfoMap.has(code)) {
+            socket.send(makeMessage(MessageTypes.SEARCH_ID, {id: code}))
+        }
     } else if (op === ListOperationTypes.DEL) {
         delVideo(id);
     } else if (op === ListOperationTypes.MOVE) {
@@ -343,7 +371,8 @@ function makeSearchResult(item) {
         socket.send(makeMessage(MessageTypes.LIST_OPERATION, {
             op: ListOperationTypes.ADD,
             code: videoId
-        }))
+        }));
+        codeInfoMap.set(videoId, item)
     }
     searchResult.addEventListener("click", onClickHandler)
     searchResult.addEventListener("keydown", onClickHandler)
@@ -372,6 +401,23 @@ function searchResultProcessor(_, data) {
     }
 }
 
+function searchIdResultProcessor(_, data) {
+    const { items } = data;
+    for (const item of items) {
+        console.log(item)
+        const code = item.id;
+        codeInfoMap.set(code, item);
+        const lines = queueElement.querySelectorAll(`[data-youtubeID='${code}`)
+        for (const line of lines) {
+            if (line !== null) {
+                const id = parseInt(line.getAttribute("data-id"))
+                makeQueueLine(code, id, id)
+                line.parentElement.removeChild(line)
+            }
+        }
+    }
+}
+
 let socket;
 
 function onYTDone() {
@@ -382,6 +428,7 @@ function onYTDone() {
     resolver.register(MessageTypes.RELEASE_CONTROL, () => setLeader(false))
     resolver.register(MessageTypes.SONG_END, songEndProcessor)
     resolver.register(MessageTypes.SEARCH, searchResultProcessor)
+    resolver.register(MessageTypes.SEARCH_ID, searchIdResultProcessor)
     socket = resolver.connectSocket()
     socket.addEventListener("open", function () {
         socket.send(makeMessage(MessageTypes.STATE, null))

+ 19 - 10
static/player.html

@@ -36,8 +36,8 @@
                 </form>
                 <form id="searchVideoForm" class="form-inline my-4">
                     <div class="form-group">
-                        <label for="searchVideo">Search</label>
-                        <input id="searchVideo" class="form-control ml-2"/>
+                        <label for="searchVideo" class="sr-only">Search</label>
+                        <input id="searchVideo" class="form-control ml-2" placeholder="Search"/>
                     </div>
                 </form>
             </div>
@@ -45,8 +45,7 @@
                 <div id="searchResultList" class="list-group col">
                     <div id="searchResultTemplate" class="searchResult list-group-item list-group-item-action" hidden>
                         <div class="row">
-                            <div class="searchResultThumbnail col-3">
-                            </div>
+                            <div class="searchResultThumbnail col-3"></div>
                             <div class="col-9 searchResultTextContainer">
                                 <span class="h4 searchResultTitle">She</span><br/>
                                 <span class="searchResultChannel pr-3">Dodie - Topic</span>
@@ -63,17 +62,27 @@
             <div id="player" class="row py-4"></div>
             <div id="videoList" class="list-group">
                 <div id="videoListCardTemplate" class="videoListCard list-group-item" hidden>
-                    <button class="videoListCardMoveUp btn btn-outline-secondary"><i class="fa fa-caret-up"></i>
-                    </button>
-                    <button class="videoListCardMoveDown btn btn-outline-secondary"><i class="fa fa-caret-down"></i>
-                    </button>
-                    <span class="templateText"></span>
-                    <button class="videoListCardDelete btn btn-outline-danger"><i class="fa fa-trash-alt"></i></button>
+                    <div class="row">
+                        <div class="col-1">
+                            <div class="videoListCardMoveUp"><i class="fa fa-caret-up"></i></div>
+                            <div class="videoListCardMoveDown"><i class="fa fa-caret-down"></i></div>
+                        </div>
+                        <div class="videoListCardThumbnail col-3"></div>
+                        <div class="col-7 videoListCardTextContainer">
+                            <span class="h4 videoListCardTitle"></span><br/>
+                            <span class="videoListCardChannel pr-3"></span>
+                            <span class="videoListCardDescription"></span>
+                        </div>
+                        <div class="col-1">
+                            <button class="videoListCardDelete btn btn-outline-danger"><i class="fa fa-trash-alt"></i></button>
+                        </div>
+                    </div>
                 </div>
             </div>
         </div>
     </div>
 </div>
+<footer class="footer mb-4"><a href="https://github.com/NielsOverkamp/Chu-Chube">Made by Niels Overkamp</a></footer>
 <script src="js/main.js" type="module"></script>
 </body>