Videos.vue 9.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435
  1. <script setup lang="ts">
  2. import { defineAsyncComponent, ref } from "vue";
  3. import Toast from "toasters";
  4. import { useWebsocketsStore } from "@/stores/websockets";
  5. import { useLongJobsStore } from "@/stores/longJobs";
  6. import { useModalsStore } from "@/stores/modals";
  7. import {
  8. TableColumn,
  9. TableFilter,
  10. TableEvents,
  11. TableBulkActions
  12. } from "@/types/advancedTable";
  13. const AdvancedTable = defineAsyncComponent(
  14. () => import("@/components/AdvancedTable.vue")
  15. );
  16. const RunJobDropdown = defineAsyncComponent(
  17. () => import("@/components/RunJobDropdown.vue")
  18. );
  19. const { setJob } = useLongJobsStore();
  20. const { socket } = useWebsocketsStore();
  21. const columnDefault = ref(<TableColumn>{
  22. sortable: true,
  23. hidable: true,
  24. defaultVisibility: "shown",
  25. draggable: true,
  26. resizable: true,
  27. minWidth: 200,
  28. maxWidth: 600
  29. });
  30. const columns = ref(<TableColumn[]>[
  31. {
  32. name: "options",
  33. displayName: "Options",
  34. properties: ["_id", "youtubeId"],
  35. sortable: false,
  36. hidable: false,
  37. resizable: false,
  38. minWidth: 129,
  39. defaultWidth: 129
  40. },
  41. {
  42. name: "thumbnailImage",
  43. displayName: "Thumb",
  44. properties: ["youtubeId"],
  45. sortable: false,
  46. minWidth: 75,
  47. defaultWidth: 75,
  48. maxWidth: 75,
  49. resizable: false
  50. },
  51. {
  52. name: "youtubeId",
  53. displayName: "YouTube ID",
  54. properties: ["youtubeId"],
  55. sortProperty: "youtubeId",
  56. minWidth: 120,
  57. defaultWidth: 120
  58. },
  59. {
  60. name: "_id",
  61. displayName: "Video ID",
  62. properties: ["_id"],
  63. sortProperty: "_id",
  64. minWidth: 215,
  65. defaultWidth: 215
  66. },
  67. {
  68. name: "title",
  69. displayName: "Title",
  70. properties: ["title"],
  71. sortProperty: "title"
  72. },
  73. {
  74. name: "author",
  75. displayName: "Author",
  76. properties: ["author"],
  77. sortProperty: "author"
  78. },
  79. {
  80. name: "duration",
  81. displayName: "Duration",
  82. properties: ["duration"],
  83. sortProperty: "duration",
  84. defaultWidth: 200
  85. },
  86. {
  87. name: "createdAt",
  88. displayName: "Created At",
  89. properties: ["createdAt"],
  90. sortProperty: "createdAt",
  91. defaultWidth: 200,
  92. defaultVisibility: "hidden"
  93. },
  94. {
  95. name: "songId",
  96. displayName: "Song ID",
  97. properties: ["songId"],
  98. sortProperty: "songId",
  99. defaultWidth: 220,
  100. defaultVisibility: "hidden"
  101. }
  102. ]);
  103. const filters = ref(<TableFilter[]>[
  104. {
  105. name: "_id",
  106. displayName: "Video ID",
  107. property: "_id",
  108. filterTypes: ["exact"],
  109. defaultFilterType: "exact"
  110. },
  111. {
  112. name: "youtubeId",
  113. displayName: "YouTube ID",
  114. property: "youtubeId",
  115. filterTypes: ["contains", "exact", "regex"],
  116. defaultFilterType: "contains"
  117. },
  118. {
  119. name: "title",
  120. displayName: "Title",
  121. property: "title",
  122. filterTypes: ["contains", "exact", "regex"],
  123. defaultFilterType: "contains"
  124. },
  125. {
  126. name: "author",
  127. displayName: "Author",
  128. property: "author",
  129. filterTypes: ["contains", "exact", "regex"],
  130. defaultFilterType: "contains"
  131. },
  132. {
  133. name: "duration",
  134. displayName: "Duration",
  135. property: "duration",
  136. filterTypes: [
  137. "numberLesserEqual",
  138. "numberLesser",
  139. "numberGreater",
  140. "numberGreaterEqual",
  141. "numberEquals"
  142. ],
  143. defaultFilterType: "numberLesser"
  144. },
  145. {
  146. name: "createdAt",
  147. displayName: "Created At",
  148. property: "createdAt",
  149. filterTypes: ["datetimeBefore", "datetimeAfter"],
  150. defaultFilterType: "datetimeBefore"
  151. },
  152. {
  153. name: "importJob",
  154. displayName: "Import Job",
  155. property: "importJob",
  156. filterTypes: ["special"],
  157. defaultFilterType: "special"
  158. },
  159. {
  160. name: "songId",
  161. displayName: "Song ID",
  162. property: "songId",
  163. filterTypes: ["contains", "exact", "regex"],
  164. defaultFilterType: "contains"
  165. }
  166. ]);
  167. const events = ref(<TableEvents>{
  168. adminRoom: "youtubeVideos",
  169. updated: {
  170. event: "admin.youtubeVideo.updated",
  171. id: "youtubeVideo._id",
  172. item: "youtubeVideo"
  173. },
  174. removed: {
  175. event: "admin.youtubeVideo.removed",
  176. id: "videoId"
  177. }
  178. });
  179. const bulkActions = ref(<TableBulkActions>{ width: 200 });
  180. const jobs = ref([
  181. {
  182. name: "Recalculate all ratings",
  183. socket: "media.recalculateAllRatings"
  184. }
  185. ]);
  186. const { openModal } = useModalsStore();
  187. const editOne = song => {
  188. openModal({
  189. modal: "editSong",
  190. data: { song }
  191. });
  192. };
  193. const editMany = selectedRows => {
  194. if (selectedRows.length === 1) editOne(selectedRows[0]);
  195. else {
  196. const songs = selectedRows.map(row => ({
  197. youtubeId: row.youtubeId
  198. }));
  199. openModal({ modal: "editSongs", data: { songs } });
  200. }
  201. };
  202. const importAlbum = selectedRows => {
  203. const youtubeIds = selectedRows.map(({ youtubeId }) => youtubeId);
  204. socket.dispatch("songs.getSongsFromYoutubeIds", youtubeIds, res => {
  205. if (res.status === "success") {
  206. openModal({
  207. modal: "importAlbum",
  208. data: { songs: res.data.songs }
  209. });
  210. } else new Toast("Could not get songs.");
  211. });
  212. };
  213. const removeVideos = videoIds => {
  214. let id;
  215. let title;
  216. socket.dispatch("youtube.removeVideos", videoIds, {
  217. cb: () => {},
  218. onProgress: res => {
  219. if (res.status === "started") {
  220. id = res.id;
  221. title = res.title;
  222. }
  223. if (id)
  224. setJob({
  225. id,
  226. name: title,
  227. ...res
  228. });
  229. }
  230. });
  231. };
  232. const getDateFormatted = createdAt => {
  233. const date = new Date(createdAt);
  234. const year = date.getFullYear();
  235. const month = `${date.getMonth() + 1}`.padStart(2, "0");
  236. const day = `${date.getDate()}`.padStart(2, "0");
  237. const hour = `${date.getHours()}`.padStart(2, "0");
  238. const minute = `${date.getMinutes()}`.padStart(2, "0");
  239. return `${year}-${month}-${day} ${hour}:${minute}`;
  240. };
  241. const handleConfirmed = ({ action, params }) => {
  242. if (typeof action === "function") {
  243. if (params) action(params);
  244. else action();
  245. }
  246. };
  247. const confirmAction = ({ message, action, params }) => {
  248. openModal({
  249. modal: "confirm",
  250. data: {
  251. message,
  252. action,
  253. params,
  254. onCompleted: handleConfirmed
  255. }
  256. });
  257. };
  258. </script>
  259. <template>
  260. <div class="admin-tab container">
  261. <page-metadata title="Admin | YouTube | Videos" />
  262. <div class="card tab-info">
  263. <div class="info-row">
  264. <h1>YouTube Videos</h1>
  265. <p>Manage YouTube video cache</p>
  266. </div>
  267. <div class="button-row">
  268. <run-job-dropdown :jobs="jobs" />
  269. </div>
  270. </div>
  271. <advanced-table
  272. :column-default="columnDefault"
  273. :columns="columns"
  274. :filters="filters"
  275. :events="events"
  276. data-action="youtube.getVideos"
  277. name="admin-youtube-videos"
  278. :max-width="1140"
  279. :bulk-actions="bulkActions"
  280. >
  281. <template #column-options="slotProps">
  282. <div class="row-options">
  283. <button
  284. class="button is-primary icon-with-button material-icons"
  285. @click="
  286. openModal({
  287. modal: 'viewYoutubeVideo',
  288. data: {
  289. videoId: slotProps.item._id
  290. }
  291. })
  292. "
  293. :disabled="slotProps.item.removed"
  294. content="View Video"
  295. v-tippy
  296. >
  297. open_in_full
  298. </button>
  299. <button
  300. class="button is-primary icon-with-button material-icons"
  301. @click="editOne(slotProps.item)"
  302. :disabled="slotProps.item.removed"
  303. content="Create/edit song from video"
  304. v-tippy
  305. >
  306. music_note
  307. </button>
  308. <button
  309. class="button is-danger icon-with-button material-icons"
  310. @click.prevent="
  311. confirmAction({
  312. message:
  313. 'Removing this video will remove it from all playlists and cause a ratings recalculation.',
  314. action: removeVideos,
  315. params: slotProps.item._id
  316. })
  317. "
  318. :disabled="slotProps.item.removed"
  319. content="Delete Video"
  320. v-tippy
  321. >
  322. delete_forever
  323. </button>
  324. </div>
  325. </template>
  326. <template #column-thumbnailImage="slotProps">
  327. <song-thumbnail class="song-thumbnail" :song="slotProps.item" />
  328. </template>
  329. <template #column-youtubeId="slotProps">
  330. <a
  331. :href="
  332. 'https://www.youtube.com/watch?v=' +
  333. `${slotProps.item.youtubeId}`
  334. "
  335. target="_blank"
  336. >
  337. {{ slotProps.item.youtubeId }}
  338. </a>
  339. </template>
  340. <template #column-_id="slotProps">
  341. <span :title="slotProps.item._id">{{
  342. slotProps.item._id
  343. }}</span>
  344. </template>
  345. <template #column-title="slotProps">
  346. <span :title="slotProps.item.title">{{
  347. slotProps.item.title
  348. }}</span>
  349. </template>
  350. <template #column-author="slotProps">
  351. <span :title="slotProps.item.author">{{
  352. slotProps.item.author
  353. }}</span>
  354. </template>
  355. <template #column-duration="slotProps">
  356. <span :title="`${slotProps.item.duration}`">{{
  357. slotProps.item.duration
  358. }}</span>
  359. </template>
  360. <template #column-createdAt="slotProps">
  361. <span :title="new Date(slotProps.item.createdAt).toString()">{{
  362. getDateFormatted(slotProps.item.createdAt)
  363. }}</span>
  364. </template>
  365. <template #column-songId="slotProps">
  366. <span :title="slotProps.item.songId">{{
  367. slotProps.item.songId
  368. }}</span>
  369. </template>
  370. <template #bulk-actions="slotProps">
  371. <div class="bulk-actions">
  372. <i
  373. class="material-icons create-songs-icon"
  374. @click.prevent="editMany(slotProps.item)"
  375. content="Create/edit songs from videos"
  376. v-tippy
  377. tabindex="0"
  378. >
  379. music_note
  380. </i>
  381. <i
  382. class="material-icons import-album-icon"
  383. @click.prevent="importAlbum(slotProps.item)"
  384. content="Import album from videos"
  385. v-tippy
  386. tabindex="0"
  387. >
  388. album
  389. </i>
  390. <i
  391. class="material-icons delete-icon"
  392. @click.prevent="
  393. confirmAction({
  394. message:
  395. 'Removing these videos will remove them from all playlists and cause a ratings recalculation.',
  396. action: removeVideos,
  397. params: slotProps.item.map(video => video._id)
  398. })
  399. "
  400. content="Delete Videos"
  401. v-tippy
  402. tabindex="0"
  403. >
  404. delete_forever
  405. </i>
  406. </div>
  407. </template>
  408. </advanced-table>
  409. </div>
  410. </template>
  411. <style lang="less" scoped>
  412. :deep(.song-thumbnail) {
  413. width: 50px;
  414. height: 50px;
  415. min-width: 50px;
  416. min-height: 50px;
  417. margin: 0 auto;
  418. }
  419. </style>