Tracks.vue 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610
  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: "genre",
  225. displayName: "Genre",
  226. property: "genre",
  227. filterTypes: ["contains", "exact", "regex"],
  228. defaultFilterType: "contains"
  229. },
  230. {
  231. name: "license",
  232. displayName: "License",
  233. property: "license",
  234. filterTypes: ["contains", "exact", "regex"],
  235. defaultFilterType: "contains"
  236. },
  237. {
  238. name: "likesCount",
  239. displayName: "Likes count",
  240. property: "likesCount",
  241. filterTypes: ["contains", "exact", "regex"],
  242. defaultFilterType: "contains"
  243. },
  244. {
  245. name: "playbackCount",
  246. displayName: "Playback count",
  247. property: "playbackCount",
  248. filterTypes: ["contains", "exact", "regex"],
  249. defaultFilterType: "contains"
  250. },
  251. {
  252. name: "public",
  253. displayName: "Public",
  254. property: "public",
  255. filterTypes: ["contains", "exact", "regex"],
  256. defaultFilterType: "contains"
  257. },
  258. {
  259. name: "tagList",
  260. displayName: "Tag list",
  261. property: "tagList",
  262. filterTypes: ["contains", "exact", "regex"],
  263. defaultFilterType: "contains"
  264. },
  265. {
  266. name: "songId",
  267. displayName: "Song ID",
  268. property: "songId",
  269. filterTypes: ["contains", "exact", "regex"],
  270. defaultFilterType: "contains"
  271. },
  272. {
  273. name: "soundcloudCreatedAt",
  274. displayName: "Soundcloud Created At",
  275. property: "soundcloudCreatedAt",
  276. filterTypes: ["datetimeBefore", "datetimeAfter"],
  277. defaultFilterType: "datetimeBefore"
  278. }
  279. ]);
  280. const events = ref<TableEvents>({
  281. adminRoom: "soundcloudTracks",
  282. updated: {
  283. event: "admin.soundcloudTrack.updated",
  284. id: "soundcloudTrack._id",
  285. item: "soundcloudTrack"
  286. },
  287. removed: {
  288. event: "admin.soundcloudTrack.removed",
  289. id: "trackId"
  290. }
  291. });
  292. const bulkActions = ref<TableBulkActions>({ width: 200 });
  293. const jobs = ref([]);
  294. if (hasPermission("media.recalculateAllRatings"))
  295. jobs.value.push({
  296. name: "Recalculate all ratings",
  297. socket: "media.recalculateAllRatings"
  298. });
  299. const { openModal } = useModalsStore();
  300. const rowToSong = row => ({
  301. mediaSource: `soundcloud:${row.trackId}`
  302. });
  303. const editOne = row => {
  304. openModal({
  305. modal: "editSong",
  306. props: { song: rowToSong(row) }
  307. });
  308. };
  309. const editMany = selectedRows => {
  310. if (selectedRows.length === 1) editOne(rowToSong(selectedRows[0]));
  311. else {
  312. const songs = selectedRows.map(rowToSong);
  313. openModal({ modal: "editSong", props: { songs } });
  314. }
  315. };
  316. const importAlbum = selectedRows => {
  317. const mediaSources = selectedRows.map(
  318. ({ trackId }) => `soundcloud:${trackId}`
  319. );
  320. socket.dispatch("songs.getSongsFromMediaSources", mediaSources, res => {
  321. if (res.status === "success") {
  322. openModal({
  323. modal: "importAlbum",
  324. props: { songs: res.data.songs }
  325. });
  326. } else new Toast("Could not get songs.");
  327. });
  328. };
  329. const bulkEditPlaylist = selectedRows => {
  330. openModal({
  331. modal: "bulkEditPlaylist",
  332. props: {
  333. mediaSources: selectedRows.map(row => row.trackId)
  334. }
  335. });
  336. };
  337. const removeTracks = videoIds => {
  338. let id;
  339. let title;
  340. socket.dispatch("soundcloud.removeTracks", videoIds, {
  341. cb: () => {},
  342. onProgress: res => {
  343. if (res.status === "started") {
  344. id = res.id;
  345. title = res.title;
  346. }
  347. if (id)
  348. setJob({
  349. id,
  350. name: title,
  351. ...res
  352. });
  353. }
  354. });
  355. };
  356. </script>
  357. <template>
  358. <div class="admin-tab container">
  359. <page-metadata title="Admin | SoundCloud | Tracks" />
  360. <div class="card tab-info">
  361. <div class="info-row">
  362. <h1>SoundCloud Tracks</h1>
  363. <p>Manage SoundCloud track cache</p>
  364. </div>
  365. <div class="button-row">
  366. <run-job-dropdown :jobs="jobs" />
  367. </div>
  368. </div>
  369. <advanced-table
  370. :column-default="columnDefault"
  371. :columns="columns"
  372. :filters="filters"
  373. :events="events"
  374. data-action="soundcloud.getTracks"
  375. name="admin-soundcloud-tracks"
  376. :max-width="1140"
  377. :bulk-actions="bulkActions"
  378. >
  379. <template #column-options="slotProps">
  380. <div class="row-options">
  381. <button
  382. class="button is-primary icon-with-button material-icons"
  383. @click="
  384. openModal({
  385. modal: 'viewMedia',
  386. props: {
  387. mediaSource: `soundcloud:${slotProps.item.trackId}`
  388. }
  389. })
  390. "
  391. :disabled="slotProps.item.removed"
  392. content="View Track"
  393. v-tippy
  394. >
  395. open_in_full
  396. </button>
  397. <button
  398. v-if="
  399. hasPermission('songs.create') ||
  400. hasPermission('songs.update')
  401. "
  402. class="button is-primary icon-with-button material-icons"
  403. @click="editOne(slotProps.item)"
  404. :disabled="slotProps.item.removed"
  405. :content="
  406. !!slotProps.item.songId
  407. ? 'Edit Song'
  408. : 'Create song from track'
  409. "
  410. v-tippy
  411. >
  412. music_note
  413. </button>
  414. <button
  415. v-if="hasPermission('soundcloud.removeTracks')"
  416. class="button is-danger icon-with-button material-icons"
  417. @click.prevent="
  418. openModal({
  419. modal: 'confirm',
  420. props: {
  421. message:
  422. 'Removing this video will remove it from all playlists and cause a ratings recalculation.',
  423. onCompleted: () =>
  424. removeTracks(slotProps.item._id)
  425. }
  426. })
  427. "
  428. :disabled="slotProps.item.removed"
  429. content="Delete Video"
  430. v-tippy
  431. >
  432. delete_forever
  433. </button>
  434. </div>
  435. </template>
  436. <template #column-thumbnailImage="slotProps">
  437. <song-thumbnail
  438. class="song-thumbnail"
  439. :song="{ thumbnail: slotProps.item.artworkUrl }"
  440. />
  441. </template>
  442. <template #column-trackId="slotProps">
  443. <span :title="slotProps.item.trackId">
  444. {{ slotProps.item.trackId }}
  445. </span>
  446. </template>
  447. <template #column-_id="slotProps">
  448. <span :title="slotProps.item._id">{{
  449. slotProps.item._id
  450. }}</span>
  451. </template>
  452. <template #column-title="slotProps">
  453. <span :title="slotProps.item.title">{{
  454. slotProps.item.title
  455. }}</span>
  456. </template>
  457. <template #column-username="slotProps">
  458. <span :title="slotProps.item.username">{{
  459. slotProps.item.username
  460. }}</span>
  461. </template>
  462. <template #column-duration="slotProps">
  463. <span :title="`${slotProps.item.duration}`">{{
  464. slotProps.item.duration
  465. }}</span>
  466. </template>
  467. <template #column-createdAt="slotProps">
  468. <span :title="new Date(slotProps.item.createdAt).toString()">{{
  469. utils.getDateFormatted(slotProps.item.createdAt)
  470. }}</span>
  471. </template>
  472. <template #column-songId="slotProps">
  473. <span :title="slotProps.item.songId">{{
  474. slotProps.item.songId
  475. }}</span>
  476. </template>
  477. <template #column-genre="slotProps">
  478. <span :title="slotProps.item.genre">{{
  479. slotProps.item.genre
  480. }}</span>
  481. </template>
  482. <template #column-license="slotProps">
  483. <span :title="slotProps.item.license">{{
  484. slotProps.item.license
  485. }}</span>
  486. </template>
  487. <template #column-likesCount="slotProps">
  488. <span :title="slotProps.item.likesCount">{{
  489. slotProps.item.likesCount
  490. }}</span>
  491. </template>
  492. <template #column-playbackCount="slotProps">
  493. <span :title="slotProps.item.playbackCount">{{
  494. slotProps.item.playbackCount
  495. }}</span>
  496. </template>
  497. <template #column-public="slotProps">
  498. <span :title="slotProps.item.public">{{
  499. slotProps.item.public
  500. }}</span>
  501. </template>
  502. <template #column-tagList="slotProps">
  503. <span :title="slotProps.item.tagList">{{
  504. slotProps.item.tagList
  505. }}</span>
  506. </template>
  507. <template #column-soundcloudCreatedAt="slotProps">
  508. <span
  509. :title="
  510. new Date(slotProps.item.soundcloudCreatedAt).toString()
  511. "
  512. >{{
  513. utils.getDateFormatted(
  514. slotProps.item.soundcloudCreatedAt
  515. )
  516. }}</span
  517. >
  518. </template>
  519. <template #bulk-actions="slotProps">
  520. <div class="bulk-actions">
  521. <i
  522. v-if="
  523. hasPermission('songs.create') ||
  524. hasPermission('songs.update')
  525. "
  526. class="material-icons create-songs-icon"
  527. @click.prevent="editMany(slotProps.item)"
  528. content="Create/edit songs from tracks"
  529. v-tippy
  530. tabindex="0"
  531. >
  532. music_note
  533. </i>
  534. <i
  535. v-if="
  536. hasPermission('songs.create') ||
  537. hasPermission('songs.update')
  538. "
  539. class="material-icons import-album-icon"
  540. @click.prevent="importAlbum(slotProps.item)"
  541. content="Import album from tracks"
  542. v-tippy
  543. tabindex="0"
  544. >
  545. album
  546. </i>
  547. <i
  548. v-if="hasPermission('playlists.songs.add')"
  549. class="material-icons playlist-bulk-edit-icon"
  550. @click.prevent="bulkEditPlaylist(slotProps.item)"
  551. content="Add To Playlist"
  552. v-tippy
  553. tabindex="0"
  554. >
  555. playlist_add
  556. </i>
  557. <i
  558. v-if="hasPermission('soundcloud.removeTracks')"
  559. class="material-icons delete-icon"
  560. @click.prevent="
  561. openModal({
  562. modal: 'confirm',
  563. props: {
  564. message:
  565. 'Removing these tracks will remove them from all playlists and cause a ratings recalculation.',
  566. onCompleted: () =>
  567. removeTracks(
  568. slotProps.item.map(
  569. video => video._id
  570. )
  571. )
  572. }
  573. })
  574. "
  575. content="Delete Videos"
  576. v-tippy
  577. tabindex="0"
  578. >
  579. delete_forever
  580. </i>
  581. </div>
  582. </template>
  583. </advanced-table>
  584. </div>
  585. </template>
  586. <style lang="less" scoped>
  587. :deep(.song-thumbnail) {
  588. width: 50px;
  589. height: 50px;
  590. min-width: 50px;
  591. min-height: 50px;
  592. margin: 0 auto;
  593. }
  594. </style>