ImportPlaylists.vue 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340
  1. <script setup lang="ts">
  2. import Toast from "toasters";
  3. import { storeToRefs } from "pinia";
  4. import { ref } from "vue";
  5. import { useSearchYoutube } from "@/composables/useSearchYoutube";
  6. import { useSearchSoundcloud } from "@/composables/useSearchSoundcloud";
  7. import { useSearchSpotify } from "@/composables/useSearchSpotify";
  8. import { useWebsocketsStore } from "@/stores/websockets";
  9. import { useLongJobsStore } from "@/stores/longJobs";
  10. import { useEditPlaylistStore } from "@/stores/editPlaylist";
  11. const props = defineProps({
  12. modalUuid: { type: String, required: true }
  13. });
  14. const { socket } = useWebsocketsStore();
  15. const editPlaylistStore = useEditPlaylistStore({ modalUuid: props.modalUuid });
  16. const { playlist } = storeToRefs(editPlaylistStore);
  17. const { setJob } = useLongJobsStore();
  18. const { youtubeSearch } = useSearchYoutube();
  19. const { soundcloudSearch } = useSearchSoundcloud();
  20. const { spotifySearch } = useSearchSpotify();
  21. const importMusarePlaylistFileInput = ref();
  22. const importMusarePlaylistFileContents = ref(null);
  23. const importYoutubePlaylist = () => {
  24. let id;
  25. let title;
  26. // import query is blank
  27. if (!youtubeSearch.value.playlist.query)
  28. return new Toast("Please enter a YouTube playlist URL.");
  29. const playlistRegex = /[\\?&]list=([^&#]*)/;
  30. const channelRegex =
  31. /\.[\w]+\/(?:(?:channel\/(UC[0-9A-Za-z_-]{21}[AQgw]))|(?:user\/?([\w-]+))|(?:c\/?([\w-]+))|(?:\/?([\w-]+)))/;
  32. if (
  33. !playlistRegex.exec(youtubeSearch.value.playlist.query) &&
  34. !channelRegex.exec(youtubeSearch.value.playlist.query)
  35. ) {
  36. return new Toast({
  37. content: "Please enter a valid YouTube playlist URL.",
  38. timeout: 4000
  39. });
  40. }
  41. return socket.dispatch(
  42. "playlists.addYoutubeSetToPlaylist",
  43. youtubeSearch.value.playlist.query,
  44. playlist.value._id,
  45. youtubeSearch.value.playlist.isImportingOnlyMusic,
  46. {
  47. cb: () => {},
  48. onProgress: res => {
  49. if (res.status === "started") {
  50. id = res.id;
  51. title = res.title;
  52. }
  53. if (id)
  54. setJob({
  55. id,
  56. name: title,
  57. ...res
  58. });
  59. }
  60. }
  61. );
  62. };
  63. const importSoundcloudPlaylist = () => {
  64. let id;
  65. let title;
  66. // import query is blank
  67. if (!soundcloudSearch.value.playlist.query)
  68. return new Toast("Please enter a SoundCloud playlist URL.");
  69. return socket.dispatch(
  70. "playlists.addSoundcloudSetToPlaylist",
  71. soundcloudSearch.value.playlist.query,
  72. playlist.value._id,
  73. {
  74. cb: () => {},
  75. onProgress: res => {
  76. if (res.status === "started") {
  77. id = res.id;
  78. title = res.title;
  79. }
  80. if (id)
  81. setJob({
  82. id,
  83. name: title,
  84. ...res
  85. });
  86. }
  87. }
  88. );
  89. };
  90. const importSpotifyPlaylist = () => {
  91. let id;
  92. let title;
  93. // import query is blank
  94. if (!spotifySearch.value.playlist.query)
  95. return new Toast("Please enter a Spotify playlist URL.");
  96. return socket.dispatch(
  97. "playlists.addSpotifySetToPlaylist",
  98. spotifySearch.value.playlist.query,
  99. playlist.value._id,
  100. {
  101. cb: () => {},
  102. onProgress: res => {
  103. if (res.status === "started") {
  104. id = res.id;
  105. title = res.title;
  106. }
  107. if (id)
  108. setJob({
  109. id,
  110. name: title,
  111. ...res
  112. });
  113. }
  114. }
  115. );
  116. };
  117. const onMusarePlaylistFileChange = () => {
  118. const reader = new FileReader();
  119. const fileInput = importMusarePlaylistFileInput.value as HTMLInputElement;
  120. const file = fileInput.files.item(0);
  121. reader.readAsText(file, "UTF-8");
  122. reader.onload = ({ target }) => {
  123. const { result } = target;
  124. try {
  125. const parsed = JSON.parse(result.toString());
  126. if (!parsed)
  127. new Toast(
  128. "An error occured whilst parsing the playlist file. Is it valid?"
  129. );
  130. else importMusarePlaylistFileContents.value = parsed;
  131. } catch (err) {
  132. new Toast(
  133. "An error occured whilst parsing the playlist file. Is it valid?"
  134. );
  135. }
  136. };
  137. reader.onerror = evt => {
  138. console.log(evt);
  139. new Toast(
  140. "An error occured whilst reading the playlist file. Is it valid?"
  141. );
  142. };
  143. };
  144. const importMusarePlaylistFile = () => {
  145. let id;
  146. let title;
  147. let mediaSources = [];
  148. if (!importMusarePlaylistFileContents.value)
  149. return new Toast("Please choose a Musare playlist file first.");
  150. if (importMusarePlaylistFileContents.value.playlist) {
  151. mediaSources =
  152. importMusarePlaylistFileContents.value.playlist.songs.map(song =>
  153. song.youtubeId ? `youtube:${song.youtubeId}` : song.mediaSource
  154. );
  155. } else if (importMusarePlaylistFileContents.value.songs) {
  156. mediaSources = importMusarePlaylistFileContents.value.songs.map(song =>
  157. song.youtubeId ? `youtube:${song.youtubeId}` : song.mediaSource
  158. );
  159. }
  160. if (mediaSources.length === 0) return new Toast("No songs to import.");
  161. return socket.dispatch(
  162. "playlists.addSongsToPlaylist",
  163. playlist.value._id,
  164. mediaSources,
  165. {
  166. cb: res => {
  167. new Toast(res.message);
  168. },
  169. onProgress: res => {
  170. if (res.status === "started") {
  171. id = res.id;
  172. title = res.title;
  173. }
  174. if (id)
  175. setJob({
  176. id,
  177. name: title,
  178. ...res
  179. });
  180. }
  181. }
  182. );
  183. };
  184. </script>
  185. <template>
  186. <div class="import-playlist-tab section">
  187. <label class="label"> Import songs from YouTube playlist </label>
  188. <div class="control is-grouped input-with-button">
  189. <p class="control is-expanded">
  190. <input
  191. class="input"
  192. type="text"
  193. placeholder="Enter YouTube Playlist URL here..."
  194. v-model="youtubeSearch.playlist.query"
  195. @keyup.enter="importYoutubePlaylist()"
  196. />
  197. </p>
  198. <p class="control has-addons">
  199. <span class="select" id="playlist-import-type">
  200. <select
  201. v-model="youtubeSearch.playlist.isImportingOnlyMusic"
  202. >
  203. <option :value="false">Import all</option>
  204. <option :value="true">Import only music</option>
  205. </select>
  206. </span>
  207. <button
  208. class="button is-info"
  209. @click.prevent="importYoutubePlaylist()"
  210. >
  211. <i class="material-icons icon-with-button">publish</i>Import
  212. </button>
  213. </p>
  214. </div>
  215. <label class="label"> Import songs from SoundCloud playlist </label>
  216. <div class="control is-grouped input-with-button">
  217. <p class="control is-expanded">
  218. <input
  219. class="input"
  220. type="text"
  221. placeholder="Enter SoundCloud Playlist URL here..."
  222. v-model="soundcloudSearch.playlist.query"
  223. @keyup.enter="importSoundcloudPlaylist()"
  224. />
  225. </p>
  226. <p class="control has-addons">
  227. <button
  228. class="button is-info"
  229. @click.prevent="importSoundcloudPlaylist()"
  230. >
  231. <i class="material-icons icon-with-button">publish</i>Import
  232. </button>
  233. </p>
  234. </div>
  235. <label class="label"> Import songs from Spotify playlist </label>
  236. <div class="control is-grouped input-with-button">
  237. <p class="control is-expanded">
  238. <input
  239. class="input"
  240. type="text"
  241. placeholder="Enter Spotify Playlist URL here..."
  242. v-model="spotifySearch.playlist.query"
  243. @keyup.enter="importSpotifyPlaylist()"
  244. />
  245. </p>
  246. <p class="control has-addons">
  247. <button
  248. class="button is-info"
  249. @click.prevent="importSpotifyPlaylist()"
  250. >
  251. <i class="material-icons icon-with-button">publish</i>Import
  252. </button>
  253. </p>
  254. </div>
  255. <label class="label"> Import songs from a Musare playlist file </label>
  256. <div class="control is-grouped input-with-button">
  257. <p class="control is-expanded">
  258. <input
  259. class="input"
  260. type="file"
  261. placeholder="Enter YouTube Playlist URL here..."
  262. @change="onMusarePlaylistFileChange"
  263. ref="importMusarePlaylistFileInput"
  264. @keyup.enter="importMusarePlaylistFile()"
  265. />
  266. </p>
  267. <p class="control">
  268. <button
  269. class="button is-info"
  270. @click.prevent="importMusarePlaylistFile()"
  271. >
  272. <i class="material-icons icon-with-button">publish</i>Import
  273. </button>
  274. </p>
  275. </div>
  276. </div>
  277. </template>
  278. <style lang="less" scoped>
  279. #playlist-import-type select {
  280. border-radius: 0;
  281. }
  282. input[type="file"] {
  283. padding-left: 0;
  284. }
  285. input[type="file"]::file-selector-button {
  286. background: var(--light-grey);
  287. border: none;
  288. height: 100%;
  289. border-right: 1px solid var(--light-grey-3);
  290. margin-right: 8px;
  291. padding: 0 8px;
  292. cursor: pointer;
  293. }
  294. input[type="file"]::file-selector-button:hover {
  295. background: var(--light-grey-2);
  296. }
  297. @media screen and (max-width: 1300px) {
  298. .import-playlist-tab #song-query-results,
  299. .section {
  300. max-width: 100% !important;
  301. }
  302. }
  303. </style>