Ver código fonte

feat: added 'load more...' button for youtube search results

Signed-off-by: Jonathan <theflametrooper@gmail.com>
Jonathan 4 anos atrás
pai
commit
190e095d37

+ 40 - 33
backend/logic/actions/apis.js

@@ -8,10 +8,11 @@ import moduleManager from "../../index";
 
 const UtilsModule = moduleManager.modules.utils;
 const IOModule = moduleManager.modules.io;
+const YouTubeModule = moduleManager.modules.youtube;
 
 export default {
 	/**
-	 * Fetches a list of songs from Youtubes API
+	 * Fetches a list of songs from Youtube's API
 	 *
 	 * @param {object} session - user session
 	 * @param {string} query - the query we'll pass to youtubes api
@@ -19,40 +20,46 @@ export default {
 	 * @returns {{status: string, data: object}} - returns an object
 	 */
 	searchYoutube(session, query, cb) {
-		const params = [
-			"part=snippet",
-			`q=${encodeURIComponent(query)}`,
-			`key=${config.get("apis.youtube.key")}`,
-			"type=video",
-			"maxResults=15"
-		].join("&");
-
-		return async.waterfall(
-			[
-				next => {
-					request(`https://www.googleapis.com/youtube/v3/search?${params}`, next);
-				},
-
-				(res, body, next) => {
-					next(null, JSON.parse(body));
-				}
-			],
-			async (err, data) => {
-				console.log(data.error);
-				if (err || data.error) {
-					if (!err) err = data.error.message;
-					err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
-					this.log(
-						"ERROR",
-						"APIS_SEARCH_YOUTUBE",
-						`Searching youtube failed with query "${query}". "${err}"`
-					);
-					return cb({ status: "failure", message: err });
-				}
+		return YouTubeModule.runJob("SEARCH", { query }, this)
+			.then(data => {
 				this.log("SUCCESS", "APIS_SEARCH_YOUTUBE", `Searching YouTube successful with query "${query}".`);
 				return cb({ status: "success", data });
-			}
-		);
+			})
+			.catch(async err => {
+				err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
+				this.log("ERROR", "APIS_SEARCH_YOUTUBE", `Searching youtube failed with query "${query}". "${err}"`);
+				return cb({ status: "failure", message: err });
+			});
+	},
+
+	/**
+	 * Fetches a list of songs from Youtube's API
+	 *
+	 * @param {object} session - user session
+	 * @param {string} query - the query we'll pass to youtubes api
+	 * @param {string} pageToken - identifies a specific page in the result set that should be retrieved
+	 * @param {Function} cb - callback
+	 * @returns {{status: string, data: object}} - returns an object
+	 */
+	searchYoutubeForPage(session, query, pageToken, cb) {
+		return YouTubeModule.runJob("SEARCH", { query, pageToken }, this)
+			.then(data => {
+				this.log(
+					"SUCCESS",
+					"APIS_SEARCH_YOUTUBE_FOR_PAGE",
+					`Searching YouTube successful with query "${query}".`
+				);
+				return cb({ status: "success", data });
+			})
+			.catch(async err => {
+				err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
+				this.log(
+					"ERROR",
+					"APIS_SEARCH_YOUTUBE_FOR_PAGE",
+					`Searching youtube failed with query "${query}". "${err}"`
+				);
+				return cb({ status: "failure", message: err });
+			});
 	},
 
 	/**

+ 37 - 0
backend/logic/youtube.js

@@ -31,6 +31,43 @@ class _YouTubeModule extends CoreClass {
 		});
 	}
 
+	/**
+	 * Fetches a list of songs from Youtube's API
+	 *
+	 * @param {object} payload - object that contains the payload
+	 * @param {string} payload.query - the query we'll pass to youtubes api
+	 * @param {string} payload.pageToken - (optional) if this exists, will search search youtube for a specific page reference
+	 * @returns {Promise} - returns promise (reject, resolve)
+	 */
+	SEARCH(payload) {
+		const params = [
+			"part=snippet",
+			`q=${encodeURIComponent(payload.query)}`,
+			`key=${config.get("apis.youtube.key")}`,
+			"type=video",
+			"maxResults=10",
+			payload.pageToken ? `pageToken=${payload.pageToken}` : null
+		].join("&");
+
+		return new Promise((resolve, reject) => {
+			request(`https://www.googleapis.com/youtube/v3/search?${params}`, (err, res, body) => {
+				if (err) {
+					YouTubeModule.log("ERROR", "SEARCH", `${err.message}`);
+					return reject(new Error("An error has occured. Please try again later."));
+				}
+
+				body = JSON.parse(body);
+
+				if (body.err) {
+					YouTubeModule.log("ERROR", "SEARCH", `${body.error.message}`);
+					return reject(new Error("An error has occured. Please try again later."));
+				}
+
+				return resolve(body);
+			});
+		});
+	}
+
 	/**
 	 * Gets the details of a song using the YouTube API
 	 *

+ 68 - 82
frontend/src/components/modals/EditPlaylist/index.vue

@@ -106,14 +106,16 @@
 								class="input"
 								type="text"
 								placeholder="Enter YouTube Playlist URL here..."
-								v-model="importQuery"
+								v-model="search.playlist.query"
 								@keyup.enter="importPlaylist()"
 							/>
 						</p>
 						<p class="control has-addons">
 							<span class="select" id="playlist-import-type">
 								<select
-									v-model="isImportingOnlyMusicOfPlaylist"
+									v-model="
+										search.playlist.isImportingOnlyMusic
+									"
 								>
 									<option :value="false">Import all</option>
 									<option :value="true">
@@ -141,7 +143,7 @@
 								class="input"
 								type="text"
 								placeholder="Enter your YouTube query here..."
-								v-model="searchSongQuery"
+								v-model="search.songs.query"
 								autofocus
 								@keyup.enter="searchForSongs()"
 							/>
@@ -158,9 +160,12 @@
 						</p>
 					</div>
 
-					<div v-if="queryResults.length > 0" id="song-query-results">
+					<div
+						v-if="search.songs.results.length > 0"
+						id="song-query-results"
+					>
 						<search-query-item
-							v-for="(result, index) in queryResults"
+							v-for="(result, index) in search.songs.results"
 							:key="index"
 							:result="result"
 						>
@@ -199,6 +204,14 @@
 								</transition>
 							</div>
 						</search-query-item>
+
+						<a
+							class="button is-default load-more-button"
+							@click.prevent="loadMoreSongs()"
+							href="#"
+						>
+							Load more...
+						</a>
 					</div>
 
 					<div class="section-margin-bottom" />
@@ -344,6 +357,8 @@ import { mapState, mapActions } from "vuex";
 import draggable from "vuedraggable";
 import Toast from "toasters";
 
+import SearchYoutube from "../../../mixins/SearchYoutube.vue";
+
 import Modal from "../../Modal.vue";
 import SearchQueryItem from "../../ui/SearchQueryItem.vue";
 import PlaylistSongItem from "./components/PlaylistSongItem.vue";
@@ -354,16 +369,12 @@ import utils from "../../../../js/utils";
 
 export default {
 	components: { Modal, draggable, SearchQueryItem, PlaylistSongItem },
+	mixins: [SearchYoutube],
 	data() {
 		return {
 			utils,
 			drag: false,
-			playlist: { songs: [] },
-			queryResults: [],
-			searchSongQuery: "",
-			directSongQuery: "",
-			importQuery: "",
-			isImportingOnlyMusicOfPlaylist: true
+			playlist: { songs: [] }
 		};
 	},
 	computed: {
@@ -440,6 +451,46 @@ export default {
 		});
 	},
 	methods: {
+		importPlaylist() {
+			let isImportingPlaylist = true;
+
+			// import query is blank
+			if (!this.search.playlist.query)
+				return new Toast({
+					content: "Please enter a YouTube playlist URL.",
+					timeout: 4000
+				});
+
+			// don't give starting import message instantly in case of instant error
+			setTimeout(() => {
+				if (isImportingPlaylist) {
+					new Toast({
+						content:
+							"Starting to import your playlist. This can take some time to do.",
+						timeout: 4000
+					});
+				}
+			}, 750);
+
+			return this.socket.emit(
+				"playlists.addSetToPlaylist",
+				this.search.playlist.query,
+				this.playlist._id,
+				this.search.playlist.isImportingOnlyMusic,
+				res => {
+					new Toast({ content: res.message, timeout: 20000 });
+					if (res.status === "success") {
+						isImportingPlaylist = false;
+						if (this.search.playlist.isImportingOnlyMusic) {
+							new Toast({
+								content: `${res.stats.songsInPlaylistTotal} of the ${res.stats.videosInPlaylistTotal} videos in the playlist were songs.`,
+								timeout: 20000
+							});
+						}
+					}
+				}
+			);
+		},
 		isEditable() {
 			return (
 				this.playlist.isUserModifiable &&
@@ -475,36 +526,6 @@ export default {
 			});
 			return this.utils.formatTimeLong(length);
 		},
-		searchForSongs() {
-			let query = this.searchSongQuery;
-			if (query.indexOf("&index=") !== -1) {
-				query = query.split("&index=");
-				query.pop();
-				query = query.join("");
-			}
-			if (query.indexOf("&list=") !== -1) {
-				query = query.split("&list=");
-				query.pop();
-				query = query.join("");
-			}
-			this.socket.emit("apis.searchYoutube", query, res => {
-				if (res.status === "success") {
-					this.queryResults = [];
-					for (let i = 0; i < res.data.items.length; i += 1) {
-						this.queryResults.push({
-							id: res.data.items[i].id.videoId,
-							url: `https://www.youtube.com/watch?v=${this.id}`,
-							title: res.data.items[i].snippet.title,
-							thumbnail:
-								res.data.items[i].snippet.thumbnails.default
-									.url,
-							isAddedToQueue: false
-						});
-					}
-				} else if (res.status === "error")
-					new Toast({ content: res.message, timeout: 3000 });
-			});
-		},
 		shuffle() {
 			this.socket.emit("playlists.shuffle", this.playlist._id, res => {
 				new Toast({ content: res.message, timeout: 4000 });
@@ -515,46 +536,6 @@ export default {
 				}
 			});
 		},
-		importPlaylist() {
-			let isImportingPlaylist = true;
-
-			// import query is blank
-			if (!this.importQuery)
-				return new Toast({
-					content: "Please enter a YouTube playlist URL.",
-					timeout: 4000
-				});
-
-			// don't give starting import message instantly in case of instant error
-			setTimeout(() => {
-				if (isImportingPlaylist) {
-					new Toast({
-						content:
-							"Starting to import your playlist. This can take some time to do.",
-						timeout: 4000
-					});
-				}
-			}, 750);
-
-			return this.socket.emit(
-				"playlists.addSetToPlaylist",
-				this.importQuery,
-				this.playlist._id,
-				this.isImportingOnlyMusicOfPlaylist,
-				res => {
-					new Toast({ content: res.message, timeout: 20000 });
-					if (res.status === "success") {
-						isImportingPlaylist = false;
-						if (this.isImportingOnlyMusicOfPlaylist) {
-							new Toast({
-								content: `${res.stats.songsInPlaylistTotal} of the ${res.stats.videosInPlaylistTotal} videos in the playlist were songs.`,
-								timeout: 20000
-							});
-						}
-					}
-				}
-			);
-		},
 		addSongToPlaylist(id, index) {
 			this.socket.emit(
 				"playlists.addSongToPlaylist",
@@ -564,7 +545,7 @@ export default {
 				res => {
 					new Toast({ content: res.message, timeout: 4000 });
 					if (res.status === "success")
-						this.queryResults[index].isAddedToQueue = true;
+						this.search.songs.results[index].isAddedToQueue = true;
 				}
 			);
 		},
@@ -810,6 +791,11 @@ export default {
 					margin-bottom: 10px;
 				}
 			}
+
+			.load-more-button {
+				width: 100%;
+				margin-top: 10px;
+			}
 		}
 	}
 

+ 81 - 0
frontend/src/mixins/SearchYoutube.vue

@@ -0,0 +1,81 @@
+<script>
+import Toast from "toasters";
+
+export default {
+	data() {
+		return {
+			search: {
+				songs: {
+					results: [],
+					query: "",
+					nextPageToken: ""
+				},
+				playlist: {
+					query: "",
+					isImportingOnlyMusic: true
+				}
+			}
+		};
+	},
+	methods: {
+		searchForSongs() {
+			let { query } = this.search.songs;
+
+			if (query.indexOf("&index=") !== -1) {
+				query = query.split("&index=");
+				query.pop();
+				query = query.join("");
+			}
+
+			if (query.indexOf("&list=") !== -1) {
+				query = query.split("&list=");
+				query.pop();
+				query = query.join("");
+			}
+
+			this.socket.emit("apis.searchYoutube", query, res => {
+				if (res.status === "success") {
+					this.search.songs.nextPageToken = res.data.nextPageToken;
+					this.search.songs.results = [];
+
+					res.data.items.forEach(result => {
+						this.search.songs.results.push({
+							id: result.id.videoId,
+							url: `https://www.youtube.com/watch?v=${this.id}`,
+							title: result.snippet.title,
+							thumbnail: result.snippet.thumbnails.default.url,
+							isAddedToQueue: false
+						});
+					});
+				} else if (res.status === "error")
+					new Toast({ content: res.message, timeout: 3000 });
+			});
+		},
+		loadMoreSongs() {
+			this.socket.emit(
+				"apis.searchYoutubeForPage",
+				this.search.songs.query,
+				this.search.songs.nextPageToken,
+				res => {
+					if (res.status === "success") {
+						this.search.songs.nextPageToken =
+							res.data.nextPageToken;
+
+						res.data.items.forEach(result => {
+							this.search.songs.results.push({
+								id: result.id.videoId,
+								url: `https://www.youtube.com/watch?v=${this.id}`,
+								title: result.snippet.title,
+								thumbnail:
+									result.snippet.thumbnails.default.url,
+								isAddedToQueue: false
+							});
+						});
+					} else if (res.status === "error")
+						new Toast({ content: res.message, timeout: 3000 });
+				}
+			);
+		}
+	}
+};
+</script>

+ 38 - 63
frontend/src/pages/Station/AddSongToQueue.vue

@@ -17,15 +17,15 @@
 							class="input"
 							type="text"
 							placeholder="Enter your YouTube query here..."
-							v-model="querySearch"
+							v-model="search.songs.query"
 							autofocus
-							@keyup.enter="submitQuery()"
+							@keyup.enter="searchForSongs()"
 						/>
 					</p>
 					<p class="control">
 						<a
 							class="button is-info"
-							@click.prevent="submitQuery()"
+							@click.prevent="searchForSongs()"
 							href="#"
 							><i class="material-icons icon-with-button"
 								>search</i
@@ -36,9 +36,12 @@
 
 				<!-- Choosing a song from youtube - query results -->
 
-				<div id="song-query-results" v-if="queryResults.length > 0">
+				<div
+					id="song-query-results"
+					v-if="search.songs.results.length > 0"
+				>
 					<search-query-item
-						v-for="(result, index) in queryResults"
+						v-for="(result, index) in search.songs.results"
 						:key="index"
 						:result="result"
 					>
@@ -75,6 +78,14 @@
 							</transition>
 						</div>
 					</search-query-item>
+
+					<a
+						class="button is-default load-more-button"
+						@click.prevent="loadMoreSongs()"
+						href="#"
+					>
+						Load more...
+					</a>
 				</div>
 
 				<!-- Import a playlist from youtube -->
@@ -97,14 +108,16 @@
 								class="input"
 								type="text"
 								placeholder="YouTube Playlist URL"
-								v-model="importQuery"
+								v-model="search.playlist.query"
 								@keyup.enter="importPlaylist()"
 							/>
 						</p>
 						<p class="control has-addons">
 							<span class="select" id="playlist-import-type">
 								<select
-									v-model="isImportingOnlyMusicOfPlaylist"
+									v-model="
+										search.playlist.isImportingOnlyMusic
+									"
 								>
 									<option :value="false">Import all</option>
 									<option :value="true"
@@ -199,6 +212,8 @@ import { mapState, mapActions } from "vuex";
 
 import Toast from "toasters";
 
+import SearchYoutube from "../../mixins/SearchYoutube.vue";
+
 import PlaylistItem from "../../components/ui/PlaylistItem.vue";
 import SearchQueryItem from "../../components/ui/SearchQueryItem.vue";
 import Modal from "../../components/Modal.vue";
@@ -207,13 +222,10 @@ import io from "../../io";
 
 export default {
 	components: { Modal, PlaylistItem, SearchQueryItem },
+	mixins: [SearchYoutube],
 	data() {
 		return {
-			querySearch: "",
-			queryResults: [],
-			playlists: [],
-			importQuery: "",
-			isImportingOnlyMusicOfPlaylist: false
+			playlists: []
 		};
 	},
 	computed: mapState({
@@ -258,7 +270,10 @@ export default {
 								timeout: 8000
 							});
 						else {
-							this.queryResults[index].isAddedToQueue = true;
+							this.search.songs.results[
+								index
+							].isAddedToQueue = true;
+
 							new Toast({
 								content: `${data.message}`,
 								timeout: 4000
@@ -274,7 +289,8 @@ export default {
 							timeout: 8000
 						});
 					else {
-						this.queryResults[index].isAddedToQueue = true;
+						this.search.songs.results[index].isAddedToQueue = true;
+
 						new Toast({
 							content: `${data.message}`,
 							timeout: 4000
@@ -287,7 +303,7 @@ export default {
 			let isImportingPlaylist = true;
 
 			// import query is blank
-			if (!this.importQuery)
+			if (!this.search.playlist.query)
 				return new Toast({
 					content: "Please enter a YouTube playlist URL.",
 					timeout: 4000
@@ -306,60 +322,14 @@ export default {
 
 			return this.socket.emit(
 				"queueSongs.addSetToQueue",
-				this.importQuery,
-				this.isImportingOnlyMusicOfPlaylist,
+				this.search.playlist.query,
+				this.search.playlist.isImportingOnlyMusic,
 				res => {
 					isImportingPlaylist = false;
 					return new Toast({ content: res.message, timeout: 20000 });
 				}
 			);
 		},
-		submitQuery() {
-			let query = this.querySearch;
-
-			if (!this.querySearch)
-				return new Toast({
-					content: "Please input a search query or a YouTube link.",
-					timeout: 4000
-				});
-
-			if (query.indexOf("&index=") !== -1) {
-				query = query.split("&index=");
-				query.pop();
-				query = query.join("");
-			}
-
-			if (query.indexOf("&list=") !== -1) {
-				query = query.split("&list=");
-				query.pop();
-				query = query.join("");
-			}
-
-			return this.socket.emit("apis.searchYoutube", query, res => {
-				if (res.status === "failure")
-					return new Toast({
-						content: "Error searching on YouTube",
-						timeout: 4000
-					});
-
-				const { data } = res;
-				this.queryResults = [];
-
-				console.log(res.data);
-
-				for (let i = 0; i < data.items.length; i += 1) {
-					this.queryResults.push({
-						id: data.items[i].id.videoId,
-						url: `https://www.youtube.com/watch?v=${this.id}`,
-						title: data.items[i].snippet.title,
-						thumbnail: data.items[i].snippet.thumbnails.default.url,
-						isAddedToQueue: false
-					});
-				}
-
-				return this.queryResults;
-			});
-		},
 		...mapActions("station", ["updatePrivatePlaylistQueueSelected"]),
 		...mapActions("user/playlists", ["editPlaylist"])
 	}
@@ -450,5 +420,10 @@ export default {
 	.search-query-item:not(:last-of-type) {
 		margin-bottom: 10px;
 	}
+
+	.load-more-button {
+		width: 100%;
+		margin-top: 10px;
+	}
 }
 </style>