ImportArtistMB.vue 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896
  1. <script setup lang="ts">
  2. /* eslint vue/no-unused-vars: 1 */
  3. /* eslint @typescript-eslint/no-unused-vars: 1 */
  4. import Toast from "toasters";
  5. import {
  6. defineAsyncComponent,
  7. ref,
  8. reactive,
  9. computed,
  10. onMounted,
  11. watch
  12. } from "vue";
  13. import { GenericResponse } from "@musare_types/actions/GenericActions";
  14. import VueJsonPretty from "vue-json-pretty";
  15. import { storeToRefs } from "pinia";
  16. import { useForm } from "@/composables/useForm";
  17. import { useWebsocketsStore } from "@/stores/websockets";
  18. import { useModalsStore } from "@/stores/modals";
  19. import { useLongJobsStore } from "@/stores/longJobs";
  20. import { useImportArtistStore } from "@/stores/importArtist";
  21. import "vue-json-pretty/lib/styles.css";
  22. import utils from "@/utils";
  23. // import { TempDraggableList } from "vue-draggable-list";
  24. const TempDraggableList = defineAsyncComponent(
  25. () => import("@/components/TempDraggableList.vue")
  26. );
  27. const TempYoutubeChannelCard = defineAsyncComponent(
  28. () => import("@/components/TempYoutubeChannelCard.vue")
  29. );
  30. const TempYoutubeVideoCard = defineAsyncComponent(
  31. () => import("@/components/TempYoutubeVideoCard.vue")
  32. );
  33. const Modal = defineAsyncComponent(() => import("@/components/Modal.vue"));
  34. const SaveButton = defineAsyncComponent(
  35. () => import("@/components/SaveButton.vue")
  36. );
  37. const props = defineProps({
  38. modalUuid: { type: String, required: true },
  39. artistId: { type: String, default: null }
  40. });
  41. const importArtistStore = useImportArtistStore({
  42. modalUuid: props.modalUuid
  43. })();
  44. const {
  45. artist,
  46. youtubeChannels,
  47. hideYoutubeChannel,
  48. youtubeVideoMap,
  49. youtubeVideoIds,
  50. linkedVideos,
  51. recordingsReleasesReleaseGroups,
  52. filterYoutubeVideos,
  53. recordingFilters,
  54. recordingSort,
  55. youtubeVideosSort,
  56. youtubeVideoTitleChanges,
  57. youtubeVideoFilters,
  58. hideYoutubeInfo,
  59. hideMusicbrainzInfo,
  60. youtubeChannelIds,
  61. // getters
  62. recordings,
  63. youtubeVideoMapAdjusted,
  64. filteredRecordings,
  65. filteredYoutubeVideoIds,
  66. filteredYoutubeVideosIdsLength
  67. } = storeToRefs(importArtistStore);
  68. const {
  69. setArtist,
  70. setYoutubeChannels,
  71. setYoutubeVideos,
  72. setMusicBrainzRecordingsReleasesReleaseGroups,
  73. linkVideos
  74. } = importArtistStore;
  75. const { socket } = useWebsocketsStore();
  76. const { closeCurrentModal } = useModalsStore();
  77. onMounted(() => {
  78. socket.onConnect(() => {
  79. if (props.artistId) {
  80. socket.dispatch(`artists.getArtistFromId`, props.artistId, res => {
  81. // res: GetArtistResponse
  82. if (res.status === "success") {
  83. setArtist(res.data.artist);
  84. socket.dispatch(
  85. `youtube.getChannelsById`,
  86. youtubeChannelIds.value,
  87. res => {
  88. // TODO handle fail
  89. const { data } = res;
  90. setYoutubeChannels(data);
  91. }
  92. );
  93. socket.dispatch(
  94. `youtube.getVideosForChannelIds`,
  95. youtubeChannelIds.value,
  96. res => {
  97. console.log(333222111, res);
  98. const { data } = res;
  99. setYoutubeVideos(data);
  100. }
  101. );
  102. socket.dispatch(
  103. `albums.getMusicBrainzRecordingsReleasesReleaseGroups`,
  104. artist.value.musicbrainzIdentifier,
  105. res => {
  106. const { data } = res;
  107. setMusicBrainzRecordingsReleasesReleaseGroups(
  108. data.releases
  109. );
  110. }
  111. );
  112. } else {
  113. new Toast("Artist with that ID not found.");
  114. closeCurrentModal();
  115. }
  116. });
  117. }
  118. });
  119. });
  120. const drag = ref(false);
  121. const repositionYoutubeVideo = () => {
  122. console.log("Reposition youtube video");
  123. };
  124. </script>
  125. <template>
  126. <modal
  127. class="import-artist-mb-modal"
  128. title="Import Artist MB"
  129. :size="'wide'"
  130. :split="true"
  131. >
  132. <template #body>
  133. <div class="columns flex flex-row w-full">
  134. <div class="column-left flex flex-column">
  135. <div class="card artist-info-card flex flex-row">
  136. <img src="/assets/notes.png" alt="Temp" />
  137. <div>
  138. <p>{{ artist.name }}</p>
  139. <p>
  140. MB:
  141. <a
  142. :href="`https://musicbrainz.org/artist/${artist.musicbrainzIdentifier}`"
  143. >{{ artist.musicbrainzIdentifier }}</a
  144. >
  145. </p>
  146. </div>
  147. </div>
  148. <div class="card recordings-card flex flex-column">
  149. <div
  150. class="flex flex-row"
  151. style="justify-content: space-between"
  152. >
  153. <p class="card-title">Recordings to display</p>
  154. <button class="button temp-button">
  155. <i class="material-icons">filter_alt</i>
  156. </button>
  157. </div>
  158. <div>
  159. Filter
  160. <div>
  161. <input
  162. type="checkbox"
  163. name="hideNullLength"
  164. id="hideNullLength"
  165. v-model="recordingFilters.hideNullLength"
  166. />
  167. <label for="hideNullLength"
  168. >Hide null length</label
  169. >
  170. </div>
  171. <div>
  172. <input
  173. type="checkbox"
  174. name="hidePartOf"
  175. id="hidePartOf"
  176. v-model="recordingFilters.hidePartOf"
  177. />
  178. <label for="hidePartOf">Hide part of</label>
  179. </div>
  180. </div>
  181. <div>
  182. Sort
  183. <div>
  184. <select
  185. name="recordingSort"
  186. id="recordingSort"
  187. v-model="recordingSort"
  188. >
  189. <option value="title">Title</option>
  190. <option value="length">Length</option>
  191. </select>
  192. </div>
  193. </div>
  194. <div>
  195. Hide info
  196. <div>
  197. <input
  198. type="checkbox"
  199. name="hideMusicbrainzInfo"
  200. id="hideMusicbrainzInfo"
  201. v-model="hideMusicbrainzInfo"
  202. />
  203. <label for="hideMusicbrainzInfo"
  204. >Hide MusicBrainz info</label
  205. >
  206. </div>
  207. </div>
  208. <div class="flex flex-column recordings">
  209. <div
  210. v-for="recording in filteredRecordings"
  211. :key="recording.id"
  212. class="recording flex flex-row"
  213. :class="recording.hide ? 'recording-hide' : ''"
  214. >
  215. <p>
  216. <span>{{ recording.title }}</span>
  217. <span v-if="recording.disambiguation"
  218. >({{ recording.disambiguation }})</span
  219. >
  220. </p>
  221. <span>{{
  222. utils.formatTime(recording.length / 1000)
  223. }}</span>
  224. <div class="icons">
  225. <tippy
  226. theme="info"
  227. v-if="!hideMusicbrainzInfo"
  228. >
  229. <i class="material-icons">info</i>
  230. <template #content>
  231. <div>
  232. <ul>
  233. <li>
  234. <p>
  235. Title:
  236. {{
  237. recording.title
  238. }}
  239. </p>
  240. </li>
  241. <li>
  242. <p>
  243. Video:
  244. {{
  245. recording.video
  246. ? "true"
  247. : "false"
  248. }}
  249. </p>
  250. </li>
  251. <li>
  252. <p>
  253. ID:
  254. {{ recording.id }}
  255. </p>
  256. </li>
  257. <li>
  258. <p>
  259. Length:
  260. {{
  261. recording.length
  262. }}
  263. </p>
  264. </li>
  265. <li
  266. v-if="
  267. recording.disambiguation
  268. "
  269. >
  270. <p>
  271. Disambiguation:
  272. {{
  273. recording.disambiguation
  274. }}
  275. </p>
  276. </li>
  277. </ul>
  278. </div>
  279. </template>
  280. </tippy>
  281. </div>
  282. </div>
  283. </div>
  284. </div>
  285. </div>
  286. <div class="column-center flex flex-column">
  287. <div class="card linking-card flex flex-column">
  288. <div
  289. class="flex flex-row"
  290. style="justify-content: space-between"
  291. >
  292. <p class="card-title">Linking</p>
  293. <div class="button-icons flex flex-row">
  294. <i class="material-icons">settings</i>
  295. <i class="material-icons">lock_open</i>
  296. </div>
  297. </div>
  298. <div>
  299. <button
  300. class="button is-primary"
  301. @click="linkVideos"
  302. >
  303. Link
  304. </button>
  305. </div>
  306. <div class="recordings flex flex-column">
  307. <div
  308. class="recording flex flex-column"
  309. v-for="recording in filteredRecordings"
  310. :key="recording.id"
  311. v-show="!recording.hide"
  312. >
  313. <div class="flex flex-row">
  314. <p>
  315. {{ recording.title }}
  316. <span v-if="recording.disambiguation"
  317. >({{
  318. recording.disambiguation
  319. }})</span
  320. >
  321. </p>
  322. <span>{{
  323. utils.formatTime(
  324. recording.length / 1000
  325. )
  326. }}</span>
  327. <div class="button-icons flex flex-row">
  328. <tippy
  329. theme="info"
  330. v-if="!hideMusicbrainzInfo"
  331. >
  332. <i class="material-icons">info</i>
  333. <template #content>
  334. <div>
  335. <ul>
  336. <li>
  337. <p>
  338. Title:
  339. {{
  340. recording.title
  341. }}
  342. </p>
  343. </li>
  344. <li>
  345. <p>
  346. Video:
  347. {{
  348. recording.video
  349. ? "true"
  350. : "false"
  351. }}
  352. </p>
  353. </li>
  354. <li>
  355. <p>
  356. ID:
  357. {{
  358. recording.id
  359. }}
  360. </p>
  361. </li>
  362. <li>
  363. <p>
  364. Length:
  365. {{
  366. recording.length
  367. }}
  368. </p>
  369. </li>
  370. <li
  371. v-if="
  372. recording.disambiguation
  373. "
  374. >
  375. <p>
  376. Disambiguation:
  377. {{
  378. recording.disambiguation
  379. }}
  380. </p>
  381. </li>
  382. </ul>
  383. </div>
  384. </template>
  385. </tippy>
  386. <button class="button temp-button">
  387. <i class="material-icons"
  388. >lock_open</i
  389. >
  390. </button>
  391. </div>
  392. </div>
  393. <div class="youtube-videos flex flex-column">
  394. <!-- <div class="youtube-video no-youtube-video" v-if="!linkedVideos[recording.id] || linkedVideos[recording.id].length === 0">
  395. <p>No videos linked</p>
  396. </div> -->
  397. <temp-draggable-list
  398. v-if="linkedVideos[recording.id]"
  399. v-model:list="
  400. linkedVideos[recording.id]
  401. "
  402. @start="drag = true"
  403. @end="drag = false"
  404. @update="repositionYoutubeVideo"
  405. group="mb-youtube-videos"
  406. :unique="true"
  407. debug-name="linked-videos"
  408. >
  409. <template
  410. #item="{
  411. element: youtubeVideoId,
  412. index
  413. }"
  414. >
  415. <temp-youtube-video-card
  416. :youtube-video="
  417. youtubeVideoMapAdjusted[
  418. youtubeVideoId
  419. ]
  420. "
  421. :hide-youtube-info="
  422. hideYoutubeInfo
  423. "
  424. :key="youtubeVideoId"
  425. ></temp-youtube-video-card>
  426. </template>
  427. <template #empty-list-placeholder>
  428. <div
  429. class="youtube-video no-youtube-video"
  430. >
  431. <p>No videos linked</p>
  432. </div>
  433. </template>
  434. </temp-draggable-list>
  435. </div>
  436. </div>
  437. </div>
  438. </div>
  439. </div>
  440. <div class="column-right flex flex-column">
  441. <div class="card youtube-channels-card">
  442. <p class="card-title">
  443. YouTube channels / playlists card
  444. </p>
  445. <div class="youtube-channels flex flex-column">
  446. <temp-youtube-channel-card
  447. v-for="youtubeChannel in youtubeChannels"
  448. :youtube-channel="youtubeChannel"
  449. :key="youtubeChannel.channelId"
  450. >
  451. </temp-youtube-channel-card>
  452. </div>
  453. </div>
  454. <div class="card youtube-videos-card">
  455. <p class="card-title">
  456. YouTube videos card ({{
  457. filteredYoutubeVideosIdsLength
  458. }}
  459. / {{ youtubeVideoIds.length }})
  460. </p>
  461. <div>
  462. Filter
  463. <div>
  464. <input
  465. type="checkbox"
  466. name="teaser"
  467. v-model="youtubeVideoFilters.teaser"
  468. />
  469. <label for="teaser">Teaser</label>
  470. </div>
  471. <div>
  472. <input
  473. type="checkbox"
  474. name="under45"
  475. v-model="youtubeVideoFilters.under45"
  476. />
  477. <label for="under45">Under 45s</label>
  478. </div>
  479. <div>
  480. <input
  481. type="checkbox"
  482. name="live"
  483. v-model="youtubeVideoFilters.live"
  484. />
  485. <label for="live">Live</label>
  486. </div>
  487. <div>
  488. <input
  489. type="checkbox"
  490. name="tour"
  491. v-model="youtubeVideoFilters.tour"
  492. />
  493. <label for="tour">Tour</label>
  494. </div>
  495. </div>
  496. <div>
  497. Name
  498. <div>
  499. <input
  500. type="checkbox"
  501. name="artistDash"
  502. v-model="
  503. youtubeVideoTitleChanges.artistDash
  504. "
  505. />
  506. <label for="artistDash">artistDash</label>
  507. </div>
  508. <div>
  509. <input
  510. type="checkbox"
  511. name="parantheses"
  512. v-model="
  513. youtubeVideoTitleChanges.parantheses
  514. "
  515. />
  516. <label for="parantheses">parantheses</label>
  517. </div>
  518. <div>
  519. <input
  520. type="checkbox"
  521. name="brackets"
  522. v-model="youtubeVideoTitleChanges.brackets"
  523. />
  524. <label for="brackets">brackets</label>
  525. </div>
  526. <div>
  527. <input
  528. type="checkbox"
  529. name="commonPhrases"
  530. v-model="
  531. youtubeVideoTitleChanges.commonPhrases
  532. "
  533. />
  534. <label for="commonPhrases"
  535. >common phrases (official mv, official
  536. audio, official video)</label
  537. >
  538. </div>
  539. </div>
  540. <div>
  541. Sort
  542. <div>
  543. <select
  544. name="youtubeVideosSort"
  545. id="youtubeVideosSort"
  546. v-model="youtubeVideosSort"
  547. >
  548. <option value="title">Title</option>
  549. <option value="length">Length</option>
  550. </select>
  551. </div>
  552. </div>
  553. <div>
  554. Hide info
  555. <div>
  556. <input
  557. type="checkbox"
  558. name="hideYoutubeInfo"
  559. id="hideYoutubeInfo"
  560. v-model="hideYoutubeInfo"
  561. />
  562. <label for="hideYoutubeInfo"
  563. >Hide YouTube info</label
  564. >
  565. </div>
  566. </div>
  567. <div>
  568. Hide YouTube channels
  569. <div
  570. v-for="youtubeChannel in youtubeChannels"
  571. :key="youtubeChannel.rawData.id"
  572. >
  573. <input
  574. type="checkbox"
  575. :name="youtubeChannel.rawData.id"
  576. :id="youtubeChannel.rawData.id"
  577. v-model="
  578. hideYoutubeChannel[
  579. youtubeChannel.rawData.id
  580. ]
  581. "
  582. />
  583. <label :for="youtubeChannel.rawData.id"
  584. >Hide {{ youtubeChannel.title }}</label
  585. >
  586. </div>
  587. </div>
  588. <div>
  589. Filter
  590. <input type="text" v-model="filterYoutubeVideos" />
  591. </div>
  592. <div class="flex flex-column youtube-videos">
  593. <temp-draggable-list
  594. v-if="filteredYoutubeVideoIds.length > 0"
  595. v-model:list="filteredYoutubeVideoIds"
  596. @start="drag = true"
  597. @end="drag = false"
  598. @update="repositionYoutubeVideo"
  599. :read-only="true"
  600. group="mb-youtube-videos"
  601. debug-name="youtube-videos"
  602. >
  603. <template
  604. #item="{ element: youtubeVideoId, index }"
  605. >
  606. <temp-youtube-video-card
  607. :youtube-video="
  608. youtubeVideoMapAdjusted[
  609. youtubeVideoId
  610. ]
  611. "
  612. :hide-youtube-info="hideYoutubeInfo"
  613. :key="youtubeVideoId"
  614. ></temp-youtube-video-card>
  615. </template>
  616. </temp-draggable-list>
  617. </div>
  618. </div>
  619. </div>
  620. </div>
  621. </template>
  622. <template #footer>
  623. <div>
  624. <!-- <save-button
  625. v-if="!importMode"
  626. :default-message="`${createArtist ? 'Create' : 'Update'} Artist`"
  627. @clicked="saveArtist()"
  628. />
  629. <save-button
  630. v-if="!importMode"
  631. :default-message="`${createArtist ? 'Create' : 'Update'} and close`"
  632. @clicked="saveArtist(true)"
  633. />
  634. <button
  635. v-if="!createArtist"
  636. class="button is-primary"
  637. @click="toggleImportMode()"
  638. >
  639. <span v-if="importMode">Edit Mode</span>
  640. <span v-else>Import Mode</span>
  641. </button> -->
  642. </div>
  643. </template>
  644. </modal>
  645. </template>
  646. <style lang="less">
  647. // .night-mode {
  648. // .edit-artist-modal {
  649. // .vjs-tree-node.is-highlight,
  650. // .vjs-tree-node:hover {
  651. // background: black;
  652. // }
  653. // }
  654. // }
  655. .import-artist-mb-modal {
  656. .modal-card {
  657. width: 100% !important;
  658. // height: 100%;
  659. }
  660. }
  661. </style>
  662. <style lang="less" scoped>
  663. .flex {
  664. display: flex;
  665. }
  666. .flex-column {
  667. flex-direction: column;
  668. }
  669. .flex-row {
  670. flex-direction: row;
  671. }
  672. .w-full {
  673. width: 100%;
  674. }
  675. .card {
  676. background-color: var(--dark-grey-3);
  677. border-radius: @border-radius;
  678. }
  679. .columns {
  680. gap: 16px;
  681. max-height: 100%;
  682. }
  683. .column-left,
  684. .column-right {
  685. width: 25%;
  686. max-height: 100%;
  687. overflow: auto;
  688. }
  689. .column-center {
  690. flex: 1;
  691. }
  692. .column-left,
  693. .column-center,
  694. .column-right {
  695. gap: 16px;
  696. }
  697. .artist-info-card {
  698. padding: 8px;
  699. gap: 8px;
  700. // Temp
  701. img {
  702. aspect-ratio: 1/1;
  703. max-width: 100px;
  704. }
  705. p:first-of-type {
  706. font-size: 20px;
  707. }
  708. p:last-of-type {
  709. font-size: 14px;
  710. }
  711. }
  712. .card-title {
  713. font-size: 20px;
  714. line-height: 24px;
  715. }
  716. .recordings-card {
  717. padding: 8px;
  718. height: 100%;
  719. gap: 16px;
  720. // font-size: 20px;
  721. .recordings {
  722. gap: 8px;
  723. .recording {
  724. background-color: var(--dark-grey-2);
  725. border-radius: @border-radius;
  726. padding: 8px;
  727. font-size: 16px;
  728. gap: 8px;
  729. align-items: center;
  730. > p {
  731. flex: 1;
  732. line-height: 24px;
  733. display: flex;
  734. flex-direction: column;
  735. // gap: 4px;
  736. span:nth-child(2) {
  737. font-size: 12px;
  738. line-height: 16px;
  739. }
  740. }
  741. .icons {
  742. height: 24px;
  743. }
  744. &.recording-hide {
  745. opacity: 0.5;
  746. }
  747. }
  748. }
  749. }
  750. .linking-card {
  751. padding: 8px;
  752. height: 100%;
  753. gap: 16px;
  754. > div > p {
  755. // font-size: 20px;
  756. // line-height: 24px;
  757. .button-icons {
  758. gap: 8px;
  759. padding: 8px;
  760. }
  761. }
  762. .recordings {
  763. gap: 8px;
  764. overflow: auto;
  765. max-height: 100%;
  766. .recording {
  767. background-color: var(--dark-grey-2);
  768. border-radius: @border-radius;
  769. padding: 8px;
  770. gap: 8px;
  771. > div:first-child {
  772. gap: 8px;
  773. align-items: center;
  774. p {
  775. font-size: 16px;
  776. line-height: 20px;
  777. flex: 1;
  778. span {
  779. font-size: 12px;
  780. line-height: 20px;
  781. }
  782. }
  783. > span {
  784. font-size: 14px;
  785. line-height: 20px;
  786. }
  787. .button-icons {
  788. }
  789. }
  790. .youtube-videos {
  791. gap: 8px;
  792. .youtube-video {
  793. background-color: var(--dark-grey);
  794. border-radius: @border-radius;
  795. gap: 8px;
  796. min-width: 350px;
  797. }
  798. .no-youtube-video {
  799. padding: 8px;
  800. }
  801. }
  802. }
  803. }
  804. }
  805. .youtube-channels-card {
  806. padding: 8px;
  807. .youtube-channels {
  808. gap: 8px;
  809. margin-top: 16px;
  810. }
  811. }
  812. .youtube-videos-card {
  813. padding: 8px;
  814. .youtube-videos {
  815. gap: 8px;
  816. margin-top: 16px;
  817. .youtube-video {
  818. background-color: var(--dark-grey);
  819. border-radius: @border-radius;
  820. gap: 8px;
  821. min-width: 350px;
  822. }
  823. }
  824. }
  825. .temp-button {
  826. padding: 0;
  827. margin: 0;
  828. background: none;
  829. color: white;
  830. outline: none;
  831. border: none;
  832. }
  833. </style>