Tracks.vue 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617
  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 { useUserAuthStore } from "@/stores/userAuth";
  8. import {
  9. TableColumn,
  10. TableFilter,
  11. TableEvents,
  12. TableBulkActions
  13. } from "@/types/advancedTable";
  14. import utils from "@/utils";
  15. const AdvancedTable = defineAsyncComponent(
  16. () => import("@/components/AdvancedTable.vue")
  17. );
  18. const RunJobDropdown = defineAsyncComponent(
  19. () => import("@/components/RunJobDropdown.vue")
  20. );
  21. const SongThumbnail = defineAsyncComponent(
  22. () => import("@/components/SongThumbnail.vue")
  23. );
  24. const { setJob } = useLongJobsStore();
  25. const { socket } = useWebsocketsStore();
  26. const userAuthStore = useUserAuthStore();
  27. const { hasPermission } = userAuthStore;
  28. const columnDefault = ref<TableColumn>({
  29. sortable: true,
  30. hidable: true,
  31. defaultVisibility: "shown",
  32. draggable: true,
  33. resizable: true,
  34. minWidth: 200,
  35. maxWidth: 600
  36. });
  37. const columns = ref<TableColumn[]>([
  38. {
  39. name: "options",
  40. displayName: "Options",
  41. properties: ["_id", "trackId", "songId"],
  42. sortable: false,
  43. hidable: false,
  44. resizable: false,
  45. minWidth:
  46. (hasPermission("songs.create") || hasPermission("songs.update")) &&
  47. hasPermission("soundcloud.removeTracks")
  48. ? 129
  49. : 85,
  50. defaultWidth:
  51. (hasPermission("songs.create") || hasPermission("songs.update")) &&
  52. hasPermission("soundcloud.removeTracks")
  53. ? 129
  54. : 85
  55. },
  56. {
  57. name: "thumbnailImage",
  58. displayName: "Thumb",
  59. properties: ["trackId", "artworkUrl"],
  60. sortable: false,
  61. minWidth: 75,
  62. defaultWidth: 75,
  63. maxWidth: 75,
  64. resizable: false
  65. },
  66. {
  67. name: "trackId",
  68. displayName: "Track ID",
  69. properties: ["trackId"],
  70. sortProperty: "trackId",
  71. minWidth: 120,
  72. defaultWidth: 120
  73. },
  74. {
  75. name: "_id",
  76. displayName: "Musare Track ID",
  77. properties: ["_id"],
  78. sortProperty: "_id",
  79. minWidth: 215,
  80. defaultWidth: 215
  81. },
  82. {
  83. name: "title",
  84. displayName: "Title",
  85. properties: ["title"],
  86. sortProperty: "title"
  87. },
  88. {
  89. name: "username",
  90. displayName: "Username",
  91. properties: ["username"],
  92. sortProperty: "username"
  93. },
  94. {
  95. name: "duration",
  96. displayName: "Duration",
  97. properties: ["duration"],
  98. sortProperty: "duration",
  99. defaultWidth: 200
  100. },
  101. {
  102. name: "createdAt",
  103. displayName: "Created At",
  104. properties: ["createdAt"],
  105. sortProperty: "createdAt",
  106. defaultWidth: 200,
  107. defaultVisibility: "hidden"
  108. },
  109. {
  110. name: "songId",
  111. displayName: "Song ID",
  112. properties: ["songId"],
  113. sortProperty: "songId",
  114. defaultWidth: 220,
  115. defaultVisibility: "hidden"
  116. },
  117. {
  118. name: "genre",
  119. displayName: "Genre",
  120. properties: ["genre"],
  121. sortProperty: "genre",
  122. defaultWidth: 200,
  123. defaultVisibility: "hidden"
  124. },
  125. {
  126. name: "license",
  127. displayName: "License",
  128. properties: ["license"],
  129. sortProperty: "license",
  130. defaultWidth: 200,
  131. defaultVisibility: "hidden"
  132. },
  133. {
  134. name: "likesCount",
  135. displayName: "Likes count",
  136. properties: ["likesCount"],
  137. sortProperty: "likesCount",
  138. defaultWidth: 200,
  139. defaultVisibility: "hidden"
  140. },
  141. {
  142. name: "playbackCount",
  143. displayName: "Playback count",
  144. properties: ["playbackCount"],
  145. sortProperty: "playbackCount",
  146. defaultWidth: 200,
  147. defaultVisibility: "hidden"
  148. },
  149. {
  150. name: "public",
  151. displayName: "Public",
  152. properties: ["public"],
  153. sortProperty: "public",
  154. defaultWidth: 200,
  155. defaultVisibility: "hidden"
  156. },
  157. {
  158. name: "tagList",
  159. displayName: "Tag list",
  160. properties: ["tagList"],
  161. sortProperty: "tagList",
  162. defaultWidth: 200,
  163. defaultVisibility: "hidden"
  164. },
  165. {
  166. name: "soundcloudCreatedAt",
  167. displayName: "Soundcloud Created At",
  168. properties: ["soundcloudCreatedAt"],
  169. sortProperty: "soundcloudCreatedAt",
  170. defaultWidth: 200,
  171. defaultVisibility: "hidden"
  172. }
  173. ]);
  174. const filters = ref<TableFilter[]>([
  175. {
  176. name: "_id",
  177. displayName: "Musare Track ID",
  178. property: "_id",
  179. filterTypes: ["exact"],
  180. defaultFilterType: "exact"
  181. },
  182. {
  183. name: "trackId",
  184. displayName: "SoundCloud ID",
  185. property: "trackId",
  186. filterTypes: ["contains", "exact", "regex"],
  187. defaultFilterType: "contains"
  188. },
  189. {
  190. name: "title",
  191. displayName: "Title",
  192. property: "title",
  193. filterTypes: ["contains", "exact", "regex"],
  194. defaultFilterType: "contains"
  195. },
  196. {
  197. name: "username",
  198. displayName: "Username",
  199. property: "username",
  200. filterTypes: ["contains", "exact", "regex"],
  201. defaultFilterType: "contains"
  202. },
  203. {
  204. name: "duration",
  205. displayName: "Duration",
  206. property: "duration",
  207. filterTypes: [
  208. "numberLesserEqual",
  209. "numberLesser",
  210. "numberGreater",
  211. "numberGreaterEqual",
  212. "numberEquals"
  213. ],
  214. defaultFilterType: "numberLesser"
  215. },
  216. {
  217. name: "createdAt",
  218. displayName: "Created At",
  219. property: "createdAt",
  220. filterTypes: ["datetimeBefore", "datetimeAfter"],
  221. defaultFilterType: "datetimeBefore"
  222. },
  223. // {
  224. // name: "importJob",
  225. // displayName: "Import Job",
  226. // property: "importJob",
  227. // filterTypes: ["special"],
  228. // defaultFilterType: "special"
  229. // },
  230. {
  231. name: "genre",
  232. displayName: "Genre",
  233. property: "genre",
  234. filterTypes: ["contains", "exact", "regex"],
  235. defaultFilterType: "contains"
  236. },
  237. {
  238. name: "license",
  239. displayName: "License",
  240. property: "license",
  241. filterTypes: ["contains", "exact", "regex"],
  242. defaultFilterType: "contains"
  243. },
  244. {
  245. name: "likesCount",
  246. displayName: "Likes count",
  247. property: "likesCount",
  248. filterTypes: ["contains", "exact", "regex"],
  249. defaultFilterType: "contains"
  250. },
  251. {
  252. name: "playbackCount",
  253. displayName: "Playback count",
  254. property: "playbackCount",
  255. filterTypes: ["contains", "exact", "regex"],
  256. defaultFilterType: "contains"
  257. },
  258. {
  259. name: "public",
  260. displayName: "Public",
  261. property: "public",
  262. filterTypes: ["contains", "exact", "regex"],
  263. defaultFilterType: "contains"
  264. },
  265. {
  266. name: "tagList",
  267. displayName: "Tag list",
  268. property: "tagList",
  269. filterTypes: ["contains", "exact", "regex"],
  270. defaultFilterType: "contains"
  271. },
  272. {
  273. name: "songId",
  274. displayName: "Song ID",
  275. property: "songId",
  276. filterTypes: ["contains", "exact", "regex"],
  277. defaultFilterType: "contains"
  278. },
  279. {
  280. name: "soundcloudCreatedAt",
  281. displayName: "Soundcloud Created At",
  282. property: "soundcloudCreatedAt",
  283. filterTypes: ["datetimeBefore", "datetimeAfter"],
  284. defaultFilterType: "datetimeBefore"
  285. }
  286. ]);
  287. const events = ref<TableEvents>({
  288. adminRoom: "soundcloudTracks",
  289. updated: {
  290. event: "admin.soundcloudTrack.updated",
  291. id: "soundcloudTrack._id",
  292. item: "soundcloudTrack"
  293. },
  294. removed: {
  295. event: "admin.soundcloudTrack.removed",
  296. id: "trackId"
  297. }
  298. });
  299. const bulkActions = ref<TableBulkActions>({ width: 200 });
  300. const jobs = ref([]);
  301. if (hasPermission("media.recalculateAllRatings"))
  302. jobs.value.push({
  303. name: "Recalculate all ratings",
  304. socket: "media.recalculateAllRatings"
  305. });
  306. const { openModal } = useModalsStore();
  307. const rowToSong = row => ({
  308. mediaSource: `soundcloud:${row.trackId}`
  309. });
  310. const editOne = row => {
  311. openModal({
  312. modal: "editSong",
  313. props: { song: rowToSong(row) }
  314. });
  315. };
  316. const editMany = selectedRows => {
  317. if (selectedRows.length === 1) editOne(rowToSong(selectedRows[0]));
  318. else {
  319. const songs = selectedRows.map(rowToSong);
  320. openModal({ modal: "editSong", props: { songs } });
  321. }
  322. };
  323. const importAlbum = selectedRows => {
  324. const mediaSources = selectedRows.map(
  325. ({ trackId }) => `soundcloud:${trackId}`
  326. );
  327. socket.dispatch("songs.getSongsFromMediaSources", mediaSources, res => {
  328. if (res.status === "success") {
  329. openModal({
  330. modal: "importAlbum",
  331. props: { songs: res.data.songs }
  332. });
  333. } else new Toast("Could not get songs.");
  334. });
  335. };
  336. const bulkEditPlaylist = selectedRows => {
  337. openModal({
  338. modal: "bulkEditPlaylist",
  339. props: {
  340. mediaSources: selectedRows.map(row => row.trackId)
  341. }
  342. });
  343. };
  344. const removeTracks = videoIds => {
  345. let id;
  346. let title;
  347. socket.dispatch("soundcloud.removeTracks", videoIds, {
  348. cb: () => {},
  349. onProgress: res => {
  350. if (res.status === "started") {
  351. id = res.id;
  352. title = res.title;
  353. }
  354. if (id)
  355. setJob({
  356. id,
  357. name: title,
  358. ...res
  359. });
  360. }
  361. });
  362. };
  363. </script>
  364. <template>
  365. <div class="admin-tab container">
  366. <page-metadata title="Admin | SoundCloud | Tracks" />
  367. <div class="card tab-info">
  368. <div class="info-row">
  369. <h1>SoundCloud Tracks</h1>
  370. <p>Manage SoundCloud track cache</p>
  371. </div>
  372. <div class="button-row">
  373. <run-job-dropdown :jobs="jobs" />
  374. </div>
  375. </div>
  376. <advanced-table
  377. :column-default="columnDefault"
  378. :columns="columns"
  379. :filters="filters"
  380. :events="events"
  381. data-action="soundcloud.getTracks"
  382. name="admin-soundcloud-tracks"
  383. :max-width="1140"
  384. :bulk-actions="bulkActions"
  385. >
  386. <template #column-options="slotProps">
  387. <div class="row-options">
  388. <button
  389. class="button is-primary icon-with-button material-icons"
  390. @click="
  391. openModal({
  392. modal: 'viewSoundcloudTrack',
  393. props: {
  394. trackId: slotProps.item.trackId
  395. }
  396. })
  397. "
  398. :disabled="slotProps.item.removed"
  399. content="View Track"
  400. v-tippy
  401. >
  402. open_in_full
  403. </button>
  404. <button
  405. v-if="
  406. hasPermission('songs.create') ||
  407. hasPermission('songs.update')
  408. "
  409. class="button is-primary icon-with-button material-icons"
  410. @click="editOne(slotProps.item)"
  411. :disabled="slotProps.item.removed"
  412. :content="
  413. !!slotProps.item.songId
  414. ? 'Edit Song'
  415. : 'Create song from track'
  416. "
  417. v-tippy
  418. >
  419. music_note
  420. </button>
  421. <button
  422. v-if="hasPermission('soundcloud.removeTracks')"
  423. class="button is-danger icon-with-button material-icons"
  424. @click.prevent="
  425. openModal({
  426. modal: 'confirm',
  427. props: {
  428. message:
  429. 'Removing this video will remove it from all playlists and cause a ratings recalculation.',
  430. onCompleted: () =>
  431. removeTracks(slotProps.item._id)
  432. }
  433. })
  434. "
  435. :disabled="slotProps.item.removed"
  436. content="Delete Video"
  437. v-tippy
  438. >
  439. delete_forever
  440. </button>
  441. </div>
  442. </template>
  443. <template #column-thumbnailImage="slotProps">
  444. <song-thumbnail
  445. class="song-thumbnail"
  446. :song="{ thumbnail: slotProps.item.artworkUrl }"
  447. />
  448. </template>
  449. <template #column-trackId="slotProps">
  450. <span :title="slotProps.item.trackId">
  451. {{ slotProps.item.trackId }}
  452. </span>
  453. </template>
  454. <template #column-_id="slotProps">
  455. <span :title="slotProps.item._id">{{
  456. slotProps.item._id
  457. }}</span>
  458. </template>
  459. <template #column-title="slotProps">
  460. <span :title="slotProps.item.title">{{
  461. slotProps.item.title
  462. }}</span>
  463. </template>
  464. <template #column-username="slotProps">
  465. <span :title="slotProps.item.username">{{
  466. slotProps.item.username
  467. }}</span>
  468. </template>
  469. <template #column-duration="slotProps">
  470. <span :title="`${slotProps.item.duration}`">{{
  471. slotProps.item.duration
  472. }}</span>
  473. </template>
  474. <template #column-createdAt="slotProps">
  475. <span :title="new Date(slotProps.item.createdAt).toString()">{{
  476. utils.getDateFormatted(slotProps.item.createdAt)
  477. }}</span>
  478. </template>
  479. <template #column-songId="slotProps">
  480. <span :title="slotProps.item.songId">{{
  481. slotProps.item.songId
  482. }}</span>
  483. </template>
  484. <template #column-genre="slotProps">
  485. <span :title="slotProps.item.genre">{{
  486. slotProps.item.genre
  487. }}</span>
  488. </template>
  489. <template #column-license="slotProps">
  490. <span :title="slotProps.item.license">{{
  491. slotProps.item.license
  492. }}</span>
  493. </template>
  494. <template #column-likesCount="slotProps">
  495. <span :title="slotProps.item.likesCount">{{
  496. slotProps.item.likesCount
  497. }}</span>
  498. </template>
  499. <template #column-playbackCount="slotProps">
  500. <span :title="slotProps.item.playbackCount">{{
  501. slotProps.item.playbackCount
  502. }}</span>
  503. </template>
  504. <template #column-public="slotProps">
  505. <span :title="slotProps.item.public">{{
  506. slotProps.item.public
  507. }}</span>
  508. </template>
  509. <template #column-tagList="slotProps">
  510. <span :title="slotProps.item.tagList">{{
  511. slotProps.item.tagList
  512. }}</span>
  513. </template>
  514. <template #column-soundcloudCreatedAt="slotProps">
  515. <span
  516. :title="
  517. new Date(slotProps.item.soundcloudCreatedAt).toString()
  518. "
  519. >{{
  520. utils.getDateFormatted(
  521. slotProps.item.soundcloudCreatedAt
  522. )
  523. }}</span
  524. >
  525. </template>
  526. <template #bulk-actions="slotProps">
  527. <div class="bulk-actions">
  528. <i
  529. v-if="
  530. hasPermission('songs.create') ||
  531. hasPermission('songs.update')
  532. "
  533. class="material-icons create-songs-icon"
  534. @click.prevent="editMany(slotProps.item)"
  535. content="Create/edit songs from tracks"
  536. v-tippy
  537. tabindex="0"
  538. >
  539. music_note
  540. </i>
  541. <i
  542. v-if="
  543. hasPermission('songs.create') ||
  544. hasPermission('songs.update')
  545. "
  546. class="material-icons import-album-icon"
  547. @click.prevent="importAlbum(slotProps.item)"
  548. content="Import album from tracks"
  549. v-tippy
  550. tabindex="0"
  551. >
  552. album
  553. </i>
  554. <i
  555. v-if="hasPermission('playlists.songs.add')"
  556. class="material-icons playlist-bulk-edit-icon"
  557. @click.prevent="bulkEditPlaylist(slotProps.item)"
  558. content="Add To Playlist"
  559. v-tippy
  560. tabindex="0"
  561. >
  562. playlist_add
  563. </i>
  564. <i
  565. v-if="hasPermission('soundcloud.removeTracks')"
  566. class="material-icons delete-icon"
  567. @click.prevent="
  568. openModal({
  569. modal: 'confirm',
  570. props: {
  571. message:
  572. 'Removing these tracks will remove them from all playlists and cause a ratings recalculation.',
  573. onCompleted: () =>
  574. removeTracks(
  575. slotProps.item.map(
  576. video => video._id
  577. )
  578. )
  579. }
  580. })
  581. "
  582. content="Delete Videos"
  583. v-tippy
  584. tabindex="0"
  585. >
  586. delete_forever
  587. </i>
  588. </div>
  589. </template>
  590. </advanced-table>
  591. </div>
  592. </template>
  593. <style lang="less" scoped>
  594. :deep(.song-thumbnail) {
  595. width: 50px;
  596. height: 50px;
  597. min-width: 50px;
  598. min-height: 50px;
  599. margin: 0 auto;
  600. }
  601. </style>