|
@@ -1,3 +1,251 @@
|
|
|
+<script setup lang="ts">
|
|
|
+import { ref } from "vue";
|
|
|
+import { useStore } from "vuex";
|
|
|
+
|
|
|
+import Toast from "toasters";
|
|
|
+
|
|
|
+import AdvancedTable from "@/components/AdvancedTable.vue";
|
|
|
+import RunJobDropdown from "@/components/RunJobDropdown.vue";
|
|
|
+
|
|
|
+const store = useStore();
|
|
|
+
|
|
|
+const { socket } = store.state.websockets;
|
|
|
+
|
|
|
+const columnDefault = ref({
|
|
|
+ sortable: true,
|
|
|
+ hidable: true,
|
|
|
+ defaultVisibility: "shown",
|
|
|
+ draggable: true,
|
|
|
+ resizable: true,
|
|
|
+ minWidth: 200,
|
|
|
+ maxWidth: 600
|
|
|
+});
|
|
|
+const columns = ref([
|
|
|
+ {
|
|
|
+ name: "options",
|
|
|
+ displayName: "Options",
|
|
|
+ properties: ["_id", "youtubeId"],
|
|
|
+ sortable: false,
|
|
|
+ hidable: false,
|
|
|
+ resizable: false,
|
|
|
+ minWidth: 129,
|
|
|
+ defaultWidth: 129
|
|
|
+ },
|
|
|
+ {
|
|
|
+ name: "thumbnailImage",
|
|
|
+ displayName: "Thumb",
|
|
|
+ properties: ["youtubeId"],
|
|
|
+ sortable: false,
|
|
|
+ minWidth: 75,
|
|
|
+ defaultWidth: 75,
|
|
|
+ maxWidth: 75,
|
|
|
+ resizable: false
|
|
|
+ },
|
|
|
+ {
|
|
|
+ name: "youtubeId",
|
|
|
+ displayName: "YouTube ID",
|
|
|
+ properties: ["youtubeId"],
|
|
|
+ sortProperty: "youtubeId",
|
|
|
+ minWidth: 120,
|
|
|
+ defaultWidth: 120
|
|
|
+ },
|
|
|
+ {
|
|
|
+ name: "_id",
|
|
|
+ displayName: "Video ID",
|
|
|
+ properties: ["_id"],
|
|
|
+ sortProperty: "_id",
|
|
|
+ minWidth: 215,
|
|
|
+ defaultWidth: 215
|
|
|
+ },
|
|
|
+ {
|
|
|
+ name: "title",
|
|
|
+ displayName: "Title",
|
|
|
+ properties: ["title"],
|
|
|
+ sortProperty: "title"
|
|
|
+ },
|
|
|
+ {
|
|
|
+ name: "author",
|
|
|
+ displayName: "Author",
|
|
|
+ properties: ["author"],
|
|
|
+ sortProperty: "author"
|
|
|
+ },
|
|
|
+ {
|
|
|
+ name: "duration",
|
|
|
+ displayName: "Duration",
|
|
|
+ properties: ["duration"],
|
|
|
+ sortProperty: "duration",
|
|
|
+ defaultWidth: 200
|
|
|
+ },
|
|
|
+ {
|
|
|
+ name: "createdAt",
|
|
|
+ displayName: "Created At",
|
|
|
+ properties: ["createdAt"],
|
|
|
+ sortProperty: "createdAt",
|
|
|
+ defaultWidth: 200,
|
|
|
+ defaultVisibility: "hidden"
|
|
|
+ }
|
|
|
+]);
|
|
|
+const filters = ref([
|
|
|
+ {
|
|
|
+ name: "_id",
|
|
|
+ displayName: "Video ID",
|
|
|
+ property: "_id",
|
|
|
+ filterTypes: ["exact"],
|
|
|
+ defaultFilterType: "exact"
|
|
|
+ },
|
|
|
+ {
|
|
|
+ name: "youtubeId",
|
|
|
+ displayName: "YouTube ID",
|
|
|
+ property: "youtubeId",
|
|
|
+ filterTypes: ["contains", "exact", "regex"],
|
|
|
+ defaultFilterType: "contains"
|
|
|
+ },
|
|
|
+ {
|
|
|
+ name: "title",
|
|
|
+ displayName: "Title",
|
|
|
+ property: "title",
|
|
|
+ filterTypes: ["contains", "exact", "regex"],
|
|
|
+ defaultFilterType: "contains"
|
|
|
+ },
|
|
|
+ {
|
|
|
+ name: "author",
|
|
|
+ displayName: "Author",
|
|
|
+ property: "author",
|
|
|
+ filterTypes: ["contains", "exact", "regex"],
|
|
|
+ defaultFilterType: "contains"
|
|
|
+ },
|
|
|
+ {
|
|
|
+ name: "duration",
|
|
|
+ displayName: "Duration",
|
|
|
+ property: "duration",
|
|
|
+ filterTypes: [
|
|
|
+ "numberLesserEqual",
|
|
|
+ "numberLesser",
|
|
|
+ "numberGreater",
|
|
|
+ "numberGreaterEqual",
|
|
|
+ "numberEquals"
|
|
|
+ ],
|
|
|
+ defaultFilterType: "numberLesser"
|
|
|
+ },
|
|
|
+ {
|
|
|
+ name: "createdAt",
|
|
|
+ displayName: "Created At",
|
|
|
+ property: "createdAt",
|
|
|
+ filterTypes: ["datetimeBefore", "datetimeAfter"],
|
|
|
+ defaultFilterType: "datetimeBefore"
|
|
|
+ },
|
|
|
+ {
|
|
|
+ name: "importJob",
|
|
|
+ displayName: "Import Job",
|
|
|
+ property: "importJob",
|
|
|
+ filterTypes: ["special"],
|
|
|
+ defaultFilterType: "special"
|
|
|
+ }
|
|
|
+]);
|
|
|
+const events = ref({
|
|
|
+ adminRoom: "youtubeVideos",
|
|
|
+ updated: {
|
|
|
+ event: "admin.youtubeVideo.updated",
|
|
|
+ id: "youtubeVideo._id",
|
|
|
+ item: "youtubeVideo"
|
|
|
+ },
|
|
|
+ removed: {
|
|
|
+ event: "admin.youtubeVideo.removed",
|
|
|
+ id: "videoId"
|
|
|
+ }
|
|
|
+});
|
|
|
+const jobs = ref([
|
|
|
+ {
|
|
|
+ name: "Recalculate all ratings",
|
|
|
+ socket: "media.recalculateAllRatings"
|
|
|
+ }
|
|
|
+]);
|
|
|
+
|
|
|
+const openModal = payload =>
|
|
|
+ store.dispatch("modalVisibility/openModal", payload);
|
|
|
+
|
|
|
+const editOne = song => {
|
|
|
+ openModal({
|
|
|
+ modal: "editSong",
|
|
|
+ data: { song }
|
|
|
+ });
|
|
|
+};
|
|
|
+
|
|
|
+const editMany = selectedRows => {
|
|
|
+ if (selectedRows.length === 1) editOne(selectedRows[0]);
|
|
|
+ else {
|
|
|
+ const songs = selectedRows.map(row => ({
|
|
|
+ youtubeId: row.youtubeId
|
|
|
+ }));
|
|
|
+ openModal({ modal: "editSongs", data: { songs } });
|
|
|
+ }
|
|
|
+};
|
|
|
+
|
|
|
+const importAlbum = selectedRows => {
|
|
|
+ const youtubeIds = selectedRows.map(({ youtubeId }) => youtubeId);
|
|
|
+ socket.dispatch("songs.getSongsFromYoutubeIds", youtubeIds, res => {
|
|
|
+ if (res.status === "success") {
|
|
|
+ openModal({
|
|
|
+ modal: "importAlbum",
|
|
|
+ data: { songs: res.data.songs }
|
|
|
+ });
|
|
|
+ } else new Toast("Could not get songs.");
|
|
|
+ });
|
|
|
+};
|
|
|
+
|
|
|
+const removeVideos = videoIds => {
|
|
|
+ let id;
|
|
|
+ let title;
|
|
|
+
|
|
|
+ socket.dispatch("youtube.removeVideos", videoIds, {
|
|
|
+ cb: () => {},
|
|
|
+ onProgress: res => {
|
|
|
+ if (res.status === "started") {
|
|
|
+ id = res.id;
|
|
|
+ title = res.title;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (id)
|
|
|
+ // TODO fix
|
|
|
+ setJob({
|
|
|
+ id,
|
|
|
+ name: title,
|
|
|
+ ...res
|
|
|
+ });
|
|
|
+ }
|
|
|
+ });
|
|
|
+};
|
|
|
+
|
|
|
+const getDateFormatted = createdAt => {
|
|
|
+ const date = new Date(createdAt);
|
|
|
+ const year = date.getFullYear();
|
|
|
+ const month = `${date.getMonth() + 1}`.padStart(2, 0);
|
|
|
+ const day = `${date.getDate()}`.padStart(2, 0);
|
|
|
+ const hour = `${date.getHours()}`.padStart(2, 0);
|
|
|
+ const minute = `${date.getMinutes()}`.padStart(2, 0);
|
|
|
+ return `${year}-${month}-${day} ${hour}:${minute}`;
|
|
|
+};
|
|
|
+
|
|
|
+const handleConfirmed = ({ action, params }) => {
|
|
|
+ if (typeof action === "function") {
|
|
|
+ if (params) action(params);
|
|
|
+ else action();
|
|
|
+ }
|
|
|
+};
|
|
|
+
|
|
|
+const confirmAction = ({ message, action, params }) => {
|
|
|
+ openModal({
|
|
|
+ modal: "confirm",
|
|
|
+ data: {
|
|
|
+ message,
|
|
|
+ action,
|
|
|
+ params,
|
|
|
+ onCompleted: handleConfirmed
|
|
|
+ }
|
|
|
+ });
|
|
|
+};
|
|
|
+</script>
|
|
|
+
|
|
|
<template>
|
|
|
<div class="admin-tab container">
|
|
|
<page-metadata title="Admin | YouTube | Videos" />
|
|
@@ -53,7 +301,7 @@
|
|
|
confirmAction({
|
|
|
message:
|
|
|
'Removing this video will remove it from all playlists and cause a ratings recalculation.',
|
|
|
- action: 'removeVideos',
|
|
|
+ action: removeVideos,
|
|
|
params: slotProps.item._id
|
|
|
})
|
|
|
"
|
|
@@ -130,7 +378,7 @@
|
|
|
confirmAction({
|
|
|
message:
|
|
|
'Removing these videos will remove them from all playlists and cause a ratings recalculation.',
|
|
|
- action: 'removeVideos',
|
|
|
+ action: removeVideos,
|
|
|
params: slotProps.item.map(video => video._id)
|
|
|
})
|
|
|
"
|
|
@@ -146,260 +394,6 @@
|
|
|
</div>
|
|
|
</template>
|
|
|
|
|
|
-<script>
|
|
|
-import { mapActions, mapGetters } from "vuex";
|
|
|
-
|
|
|
-import Toast from "toasters";
|
|
|
-
|
|
|
-import AdvancedTable from "@/components/AdvancedTable.vue";
|
|
|
-import RunJobDropdown from "@/components/RunJobDropdown.vue";
|
|
|
-
|
|
|
-export default {
|
|
|
- components: {
|
|
|
- AdvancedTable,
|
|
|
- RunJobDropdown
|
|
|
- },
|
|
|
- data() {
|
|
|
- return {
|
|
|
- columnDefault: {
|
|
|
- sortable: true,
|
|
|
- hidable: true,
|
|
|
- defaultVisibility: "shown",
|
|
|
- draggable: true,
|
|
|
- resizable: true,
|
|
|
- minWidth: 200,
|
|
|
- maxWidth: 600
|
|
|
- },
|
|
|
- columns: [
|
|
|
- {
|
|
|
- name: "options",
|
|
|
- displayName: "Options",
|
|
|
- properties: ["_id", "youtubeId"],
|
|
|
- sortable: false,
|
|
|
- hidable: false,
|
|
|
- resizable: false,
|
|
|
- minWidth: 129,
|
|
|
- defaultWidth: 129
|
|
|
- },
|
|
|
- {
|
|
|
- name: "thumbnailImage",
|
|
|
- displayName: "Thumb",
|
|
|
- properties: ["youtubeId"],
|
|
|
- sortable: false,
|
|
|
- minWidth: 75,
|
|
|
- defaultWidth: 75,
|
|
|
- maxWidth: 75,
|
|
|
- resizable: false
|
|
|
- },
|
|
|
- {
|
|
|
- name: "youtubeId",
|
|
|
- displayName: "YouTube ID",
|
|
|
- properties: ["youtubeId"],
|
|
|
- sortProperty: "youtubeId",
|
|
|
- minWidth: 120,
|
|
|
- defaultWidth: 120
|
|
|
- },
|
|
|
- {
|
|
|
- name: "_id",
|
|
|
- displayName: "Video ID",
|
|
|
- properties: ["_id"],
|
|
|
- sortProperty: "_id",
|
|
|
- minWidth: 215,
|
|
|
- defaultWidth: 215
|
|
|
- },
|
|
|
- {
|
|
|
- name: "title",
|
|
|
- displayName: "Title",
|
|
|
- properties: ["title"],
|
|
|
- sortProperty: "title"
|
|
|
- },
|
|
|
- {
|
|
|
- name: "author",
|
|
|
- displayName: "Author",
|
|
|
- properties: ["author"],
|
|
|
- sortProperty: "author"
|
|
|
- },
|
|
|
- {
|
|
|
- name: "duration",
|
|
|
- displayName: "Duration",
|
|
|
- properties: ["duration"],
|
|
|
- sortProperty: "duration",
|
|
|
- defaultWidth: 200
|
|
|
- },
|
|
|
- {
|
|
|
- name: "createdAt",
|
|
|
- displayName: "Created At",
|
|
|
- properties: ["createdAt"],
|
|
|
- sortProperty: "createdAt",
|
|
|
- defaultWidth: 200,
|
|
|
- defaultVisibility: "hidden"
|
|
|
- }
|
|
|
- ],
|
|
|
- filters: [
|
|
|
- {
|
|
|
- name: "_id",
|
|
|
- displayName: "Video ID",
|
|
|
- property: "_id",
|
|
|
- filterTypes: ["exact"],
|
|
|
- defaultFilterType: "exact"
|
|
|
- },
|
|
|
- {
|
|
|
- name: "youtubeId",
|
|
|
- displayName: "YouTube ID",
|
|
|
- property: "youtubeId",
|
|
|
- filterTypes: ["contains", "exact", "regex"],
|
|
|
- defaultFilterType: "contains"
|
|
|
- },
|
|
|
- {
|
|
|
- name: "title",
|
|
|
- displayName: "Title",
|
|
|
- property: "title",
|
|
|
- filterTypes: ["contains", "exact", "regex"],
|
|
|
- defaultFilterType: "contains"
|
|
|
- },
|
|
|
- {
|
|
|
- name: "author",
|
|
|
- displayName: "Author",
|
|
|
- property: "author",
|
|
|
- filterTypes: ["contains", "exact", "regex"],
|
|
|
- defaultFilterType: "contains"
|
|
|
- },
|
|
|
- {
|
|
|
- name: "duration",
|
|
|
- displayName: "Duration",
|
|
|
- property: "duration",
|
|
|
- filterTypes: [
|
|
|
- "numberLesserEqual",
|
|
|
- "numberLesser",
|
|
|
- "numberGreater",
|
|
|
- "numberGreaterEqual",
|
|
|
- "numberEquals"
|
|
|
- ],
|
|
|
- defaultFilterType: "numberLesser"
|
|
|
- },
|
|
|
- {
|
|
|
- name: "createdAt",
|
|
|
- displayName: "Created At",
|
|
|
- property: "createdAt",
|
|
|
- filterTypes: ["datetimeBefore", "datetimeAfter"],
|
|
|
- defaultFilterType: "datetimeBefore"
|
|
|
- },
|
|
|
- {
|
|
|
- name: "importJob",
|
|
|
- displayName: "Import Job",
|
|
|
- property: "importJob",
|
|
|
- filterTypes: ["special"],
|
|
|
- defaultFilterType: "special"
|
|
|
- }
|
|
|
- ],
|
|
|
- events: {
|
|
|
- adminRoom: "youtubeVideos",
|
|
|
- updated: {
|
|
|
- event: "admin.youtubeVideo.updated",
|
|
|
- id: "youtubeVideo._id",
|
|
|
- item: "youtubeVideo"
|
|
|
- },
|
|
|
- removed: {
|
|
|
- event: "admin.youtubeVideo.removed",
|
|
|
- id: "videoId"
|
|
|
- }
|
|
|
- },
|
|
|
- jobs: [
|
|
|
- {
|
|
|
- name: "Recalculate all ratings",
|
|
|
- socket: "media.recalculateAllRatings"
|
|
|
- }
|
|
|
- ]
|
|
|
- };
|
|
|
- },
|
|
|
- computed: {
|
|
|
- ...mapGetters({
|
|
|
- socket: "websockets/getSocket"
|
|
|
- })
|
|
|
- },
|
|
|
- methods: {
|
|
|
- editOne(song) {
|
|
|
- this.openModal({
|
|
|
- modal: "editSong",
|
|
|
- data: { song }
|
|
|
- });
|
|
|
- },
|
|
|
- editMany(selectedRows) {
|
|
|
- if (selectedRows.length === 1) this.editOne(selectedRows[0]);
|
|
|
- else {
|
|
|
- const songs = selectedRows.map(row => ({
|
|
|
- youtubeId: row.youtubeId
|
|
|
- }));
|
|
|
- this.openModal({ modal: "editSongs", data: { songs } });
|
|
|
- }
|
|
|
- },
|
|
|
- importAlbum(selectedRows) {
|
|
|
- const youtubeIds = selectedRows.map(({ youtubeId }) => youtubeId);
|
|
|
- this.socket.dispatch(
|
|
|
- "songs.getSongsFromYoutubeIds",
|
|
|
- youtubeIds,
|
|
|
- res => {
|
|
|
- if (res.status === "success") {
|
|
|
- this.openModal({
|
|
|
- modal: "importAlbum",
|
|
|
- data: { songs: res.data.songs }
|
|
|
- });
|
|
|
- } else new Toast("Could not get songs.");
|
|
|
- }
|
|
|
- );
|
|
|
- },
|
|
|
- removeVideos(videoIds) {
|
|
|
- let id;
|
|
|
- let title;
|
|
|
-
|
|
|
- this.socket.dispatch("youtube.removeVideos", videoIds, {
|
|
|
- cb: () => {},
|
|
|
- onProgress: res => {
|
|
|
- if (res.status === "started") {
|
|
|
- id = res.id;
|
|
|
- title = res.title;
|
|
|
- }
|
|
|
-
|
|
|
- if (id)
|
|
|
- this.setJob({
|
|
|
- id,
|
|
|
- name: title,
|
|
|
- ...res
|
|
|
- });
|
|
|
- }
|
|
|
- });
|
|
|
- },
|
|
|
- getDateFormatted(createdAt) {
|
|
|
- const date = new Date(createdAt);
|
|
|
- const year = date.getFullYear();
|
|
|
- const month = `${date.getMonth() + 1}`.padStart(2, 0);
|
|
|
- const day = `${date.getDate()}`.padStart(2, 0);
|
|
|
- const hour = `${date.getHours()}`.padStart(2, 0);
|
|
|
- const minute = `${date.getMinutes()}`.padStart(2, 0);
|
|
|
- return `${year}-${month}-${day} ${hour}:${minute}`;
|
|
|
- },
|
|
|
- confirmAction({ message, action, params }) {
|
|
|
- this.openModal({
|
|
|
- modal: "confirm",
|
|
|
- data: {
|
|
|
- message,
|
|
|
- action,
|
|
|
- params,
|
|
|
- onCompleted: this.handleConfirmed
|
|
|
- }
|
|
|
- });
|
|
|
- },
|
|
|
- handleConfirmed({ action, params }) {
|
|
|
- if (typeof this[action] === "function") {
|
|
|
- if (params) this[action](params);
|
|
|
- else this[action]();
|
|
|
- }
|
|
|
- },
|
|
|
- ...mapActions("modalVisibility", ["openModal"])
|
|
|
- }
|
|
|
-};
|
|
|
-</script>
|
|
|
-
|
|
|
<style lang="less" scoped>
|
|
|
:deep(.song-thumbnail) {
|
|
|
width: 50px;
|