EditSong.vue 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774
  1. <template>
  2. <div>
  3. <modal title="Edit Song">
  4. <div slot="body">
  5. <h5 class="has-text-centered">Video Preview</h5>
  6. <div class="video-container">
  7. <div id="player"></div>
  8. <div class="controls">
  9. <form action="#">
  10. <p style="margin-top: 0; position: relative;">
  11. <input
  12. type="range"
  13. id="volumeSlider"
  14. min="0"
  15. max="100"
  16. class="active"
  17. v-on:change="changeVolume()"
  18. v-on:input="changeVolume()"
  19. />
  20. </p>
  21. </form>
  22. <p class="control has-addons">
  23. <button
  24. class="button"
  25. v-on:click="settings('pause')"
  26. v-if="!video.paused"
  27. >
  28. <i class="material-icons">pause</i>
  29. </button>
  30. <button
  31. class="button"
  32. v-on:click="settings('play')"
  33. v-if="video.paused"
  34. >
  35. <i class="material-icons">play_arrow</i>
  36. </button>
  37. <button
  38. class="button"
  39. v-on:click="settings('stop')"
  40. >
  41. <i class="material-icons">stop</i>
  42. </button>
  43. <button
  44. class="button"
  45. v-on:click="settings('skipToLast10Secs')"
  46. >
  47. <i class="material-icons">fast_forward</i>
  48. </button>
  49. </p>
  50. <p>
  51. YouTube:
  52. <span>{{ youtubeVideoCurrentTime }}</span> /
  53. <span>{{ youtubeVideoDuration }}</span>
  54. {{ youtubeVideoNote }}
  55. </p>
  56. </div>
  57. </div>
  58. <h5 class="has-text-centered">Thumbnail Preview</h5>
  59. <img
  60. class="thumbnail-preview"
  61. :src="editing.song.thumbnail"
  62. onerror="this.src='/assets/notes-transparent.png'"
  63. />
  64. <div class="control is-horizontal">
  65. <div class="control-label">
  66. <label class="label">Thumbnail URL</label>
  67. </div>
  68. <div class="control">
  69. <input
  70. class="input"
  71. type="text"
  72. v-model="editing.song.thumbnail"
  73. />
  74. </div>
  75. </div>
  76. <h5 class="has-text-centered">Edit Information</h5>
  77. <p class="control">
  78. <label class="checkbox">
  79. <input
  80. type="checkbox"
  81. v-model="editing.song.explicit"
  82. />
  83. Explicit
  84. </label>
  85. </p>
  86. <label class="label">Song ID & Title</label>
  87. <div class="control is-horizontal">
  88. <div class="control is-grouped">
  89. <p class="control is-expanded">
  90. <input
  91. class="input"
  92. type="text"
  93. v-model="editing.song.songId"
  94. />
  95. </p>
  96. <p class="control is-expanded">
  97. <input
  98. class="input"
  99. type="text"
  100. v-model="editing.song.title"
  101. autofocus
  102. />
  103. </p>
  104. </div>
  105. </div>
  106. <label class="label">Artists & Genres</label>
  107. <div class="control is-horizontal">
  108. <div class="control is-grouped artist-genres">
  109. <div>
  110. <p class="control has-addons">
  111. <input
  112. class="input"
  113. id="new-artist"
  114. type="text"
  115. placeholder="Artist"
  116. />
  117. <button
  118. class="button is-info"
  119. v-on:click="addTag('artists')"
  120. >
  121. Add Artist
  122. </button>
  123. </p>
  124. <span
  125. class="tag is-info"
  126. v-for="(artist, index) in editing.song.artists"
  127. :key="index"
  128. >
  129. {{ artist }}
  130. <button
  131. class="delete is-info"
  132. v-on:click="removeTag('artists', index)"
  133. ></button>
  134. </span>
  135. </div>
  136. <div>
  137. <p class="control has-addons">
  138. <input
  139. class="input"
  140. id="new-genre"
  141. type="text"
  142. placeholder="Genre"
  143. />
  144. <button
  145. class="button is-info"
  146. v-on:click="addTag('genres')"
  147. >
  148. Add Genre
  149. </button>
  150. </p>
  151. <span
  152. class="tag is-info"
  153. v-for="(genre, index) in editing.song.genres"
  154. :key="index"
  155. >
  156. {{ genre }}
  157. <button
  158. class="delete is-info"
  159. v-on:click="removeTag('genres', index)"
  160. ></button>
  161. </span>
  162. </div>
  163. </div>
  164. </div>
  165. <label class="label">Song Duration</label>
  166. <p class="control">
  167. <input
  168. class="input"
  169. type="text"
  170. v-model="editing.song.duration"
  171. />
  172. </p>
  173. <label class="label">Skip Duration</label>
  174. <p class="control">
  175. <input
  176. class="input"
  177. type="text"
  178. v-model="editing.song.skipDuration"
  179. />
  180. </p>
  181. <article class="message" v-if="editing.type === 'songs'">
  182. <div class="message-body">
  183. <span class="reports-length">
  184. {{ reports.length }}
  185. <span
  186. v-if="reports.length > 1 || reports.length <= 0"
  187. >&nbsp;Reports</span
  188. >
  189. <span v-else>&nbsp;Report</span>
  190. </span>
  191. <div v-for="(report, index) in reports" :key="index">
  192. <a
  193. :href="`/admin/reports?id=${report}`"
  194. class="report-link"
  195. >Report - {{ report }}</a
  196. >
  197. </div>
  198. </div>
  199. </article>
  200. <hr />
  201. <h5 class="has-text-centered">Spotify Information</h5>
  202. <label class="label">Song title</label>
  203. <p class="control">
  204. <input class="input" type="text" v-model="spotify.title" />
  205. </p>
  206. <label class="label">Song artist (1 artist full name)</label>
  207. <p class="control">
  208. <input class="input" type="text" v-model="spotify.artist" />
  209. </p>
  210. <button
  211. class="button is-success"
  212. v-on:click="getSpotifySongs()"
  213. >
  214. Get Spotify songs
  215. </button>
  216. <hr />
  217. <article
  218. class="media"
  219. v-for="(song, index) in spotify.songs"
  220. :key="index"
  221. >
  222. <figure class="media-left">
  223. <p class="image is-64x64">
  224. <img
  225. :src="song.thumbnail"
  226. onerror="this.src='/assets/notes-transparent.png'"
  227. />
  228. </p>
  229. </figure>
  230. <div class="media-content">
  231. <div class="content">
  232. <p>
  233. <strong>{{ song.title }}</strong>
  234. <br />
  235. <small>Artists: {{ song.artists }}</small
  236. >, <small>Duration: {{ song.duration }}</small
  237. >,
  238. <small>Explicit: {{ song.explicit }}</small>
  239. <br />
  240. <small>Thumbnail: {{ song.thumbnail }}</small>
  241. </p>
  242. </div>
  243. </div>
  244. </article>
  245. </div>
  246. <div slot="footer">
  247. <button
  248. class="button is-success"
  249. v-on:click="save(editing.song, false)"
  250. >
  251. <i class="material-icons save-changes">done</i>
  252. <span>&nbsp;Save</span>
  253. </button>
  254. <button
  255. class="button is-success"
  256. v-on:click="save(editing.song, true)"
  257. >
  258. <i class="material-icons save-changes">done</i>
  259. <span>&nbsp;Save and close</span>
  260. </button>
  261. <button
  262. class="button is-danger"
  263. v-on:click="
  264. toggleModal({ sector: 'admin', modal: 'editSong' })
  265. "
  266. >
  267. <span>&nbsp;Close</span>
  268. </button>
  269. </div>
  270. </modal>
  271. </div>
  272. </template>
  273. <script>
  274. import { mapState, mapActions } from "vuex";
  275. import io from "../../io";
  276. import validation from "../../validation";
  277. import { Toast } from "vue-roaster";
  278. import Modal from "./Modal.vue";
  279. export default {
  280. components: { Modal },
  281. data() {
  282. return {
  283. reports: 0,
  284. spotify: {
  285. title: "",
  286. artist: "",
  287. songs: []
  288. },
  289. youtubeVideoDuration: 0.0,
  290. youtubeVideoCurrentTime: 0.0,
  291. youtubeVideoNote: "",
  292. useHTTPS: false
  293. };
  294. },
  295. computed: {
  296. ...mapState("admin/songs", {
  297. video: state => state.video,
  298. editing: state => state.editing
  299. }),
  300. ...mapState("modals", {
  301. modals: state => state.modals.admin
  302. })
  303. },
  304. methods: {
  305. save: function(song, close) {
  306. let _this = this;
  307. if (!song.title)
  308. return Toast.methods.addToast(
  309. "Please fill in all fields",
  310. 8000
  311. );
  312. if (!song.thumbnail)
  313. return Toast.methods.addToast(
  314. "Please fill in all fields",
  315. 8000
  316. );
  317. // Duration
  318. if (
  319. Number(song.skipDuration) + Number(song.duration) >
  320. this.youtubeVideoDuration
  321. ) {
  322. return Toast.methods.addToast(
  323. "Duration can't be higher than the length of the video",
  324. 8000
  325. );
  326. }
  327. // Title
  328. if (!validation.isLength(song.title, 1, 64))
  329. return Toast.methods.addToast(
  330. "Title must have between 1 and 64 characters.",
  331. 8000
  332. );
  333. if (!validation.regex.ascii.test(song.title))
  334. return Toast.methods.addToast(
  335. "Invalid title format. Only ascii characters are allowed.",
  336. 8000
  337. );
  338. // Artists
  339. if (song.artists.length < 1 || song.artists.length > 10)
  340. return Toast.methods.addToast(
  341. "Invalid artists. You must have at least 1 artist and a maximum of 10 artists.",
  342. 8000
  343. );
  344. let error;
  345. song.artists.forEach(artist => {
  346. if (!validation.isLength(artist, 1, 32))
  347. return (error =
  348. "Artist must have between 1 and 32 characters.");
  349. if (!validation.regex.ascii.test(artist))
  350. return (error =
  351. "Invalid artist format. Only ascii characters are allowed.");
  352. if (artist === "NONE")
  353. return (error =
  354. 'Invalid artist format. Artists are not allowed to be named "NONE".');
  355. });
  356. if (error) return Toast.methods.addToast(error, 8000);
  357. // Genres
  358. error = undefined;
  359. song.genres.forEach(genre => {
  360. if (!validation.isLength(genre, 1, 16))
  361. return (error =
  362. "Genre must have between 1 and 16 characters.");
  363. if (!validation.regex.az09_.test(genre))
  364. return (error =
  365. "Invalid genre format. Only ascii characters are allowed.");
  366. });
  367. if (error) return Toast.methods.addToast(error, 8000);
  368. // Thumbnail
  369. if (!validation.isLength(song.thumbnail, 8, 256))
  370. return Toast.methods.addToast(
  371. "Thumbnail must have between 8 and 256 characters.",
  372. 8000
  373. );
  374. if (this.useHTTPS && song.thumbnail.indexOf("https://") !== 0) {
  375. return Toast.methods.addToast(
  376. 'Thumbnail must start with "https://".',
  377. 8000
  378. );
  379. }
  380. if (!this.useHTTPS && song.thumbnail.indexOf("http://") !== 0) {
  381. return Toast.methods.addToast(
  382. 'Thumbnail must start with "http://".',
  383. 8000
  384. );
  385. }
  386. this.socket.emit(
  387. `${_this.editing.type}.update`,
  388. song._id,
  389. song,
  390. res => {
  391. Toast.methods.addToast(res.message, 4000);
  392. if (res.status === "success") {
  393. _this.$parent.songs.forEach(lSong => {
  394. if (song._id === lSong._id) {
  395. for (let n in song) {
  396. lSong[n] = song[n];
  397. }
  398. }
  399. });
  400. }
  401. if (close) _this.closeCurrentModal();
  402. }
  403. );
  404. },
  405. settings: function(type) {
  406. let _this = this;
  407. switch (type) {
  408. case "stop":
  409. _this.stopVideo();
  410. _this.pauseVideo(true);
  411. break;
  412. case "pause":
  413. _this.pauseVideo(true);
  414. break;
  415. case "play":
  416. _this.pauseVideo(false);
  417. break;
  418. case "skipToLast10Secs":
  419. _this.video.player.seekTo(
  420. _this.editing.song.duration -
  421. 10 +
  422. _this.editing.song.skipDuration
  423. );
  424. break;
  425. }
  426. },
  427. changeVolume: function() {
  428. let local = this;
  429. let volume = document.getElementById("volumeSlider").value;
  430. localStorage.setItem("volume", volume);
  431. local.video.player.setVolume(volume);
  432. if (volume > 0) local.video.player.unMute();
  433. },
  434. addTag: function(type) {
  435. if (type == "genres") {
  436. let genre = document
  437. .getElementById("new-genre")
  438. .value.toLowerCase()
  439. .trim();
  440. if (this.editing.song.genres.indexOf(genre) !== -1)
  441. return Toast.methods.addToast("Genre already exists", 3000);
  442. if (genre) {
  443. this.editing.song.genres.push(genre);
  444. document.getElementById("new-genre").value = "";
  445. } else Toast.methods.addToast("Genre cannot be empty", 3000);
  446. } else if (type == "artists") {
  447. let artist = document.getElementById("new-artist").value;
  448. if (this.editing.song.artists.indexOf(artist) !== -1)
  449. return Toast.methods.addToast(
  450. "Artist already exists",
  451. 3000
  452. );
  453. if (document.getElementById("new-artist").value !== "") {
  454. this.editing.song.artists.push(artist);
  455. document.getElementById("new-artist").value = "";
  456. } else Toast.methods.addToast("Artist cannot be empty", 3000);
  457. }
  458. },
  459. removeTag: function(type, index) {
  460. if (type == "genres") this.editing.song.genres.splice(index, 1);
  461. else if (type == "artists")
  462. this.editing.song.artists.splice(index, 1);
  463. },
  464. getSpotifySongs: function() {
  465. this.socket.emit(
  466. "apis.getSpotifySongs",
  467. this.spotify.title,
  468. this.spotify.artist,
  469. res => {
  470. if (res.status === "success") {
  471. Toast.methods.addToast(
  472. `Succesfully got ${res.songs.length} song${
  473. res.songs.length !== 1 ? "s" : ""
  474. }.`,
  475. 3000
  476. );
  477. this.spotify.songs = res.songs;
  478. } else
  479. Toast.methods.addToast(
  480. `Failed to get songs. ${res.message}`,
  481. 3000
  482. );
  483. }
  484. );
  485. },
  486. ...mapActions("admin/songs", [
  487. "stopVideo",
  488. "loadVideoById",
  489. "pauseVideo",
  490. "getCurrentTime",
  491. "editSong"
  492. ]),
  493. ...mapActions("modals", ["toggleModal", "closeCurrentModal"])
  494. },
  495. mounted: function() {
  496. let _this = this;
  497. // if (this.modals.editSong = false) this.video.player.stopVideo();
  498. // this.loadVideoById(
  499. // this.editing.song.songId,
  500. // this.editing.song.skipDuration
  501. // );
  502. lofig.get("cookie.secure", res => {
  503. _this.useHTTPS = res;
  504. });
  505. io.getSocket(socket => (_this.socket = socket));
  506. setInterval(() => {
  507. if (
  508. _this.video.paused === false &&
  509. _this.playerReady &&
  510. _this.video.player.getCurrentTime() -
  511. _this.editing.song.skipDuration >
  512. _this.editing.song.duration
  513. ) {
  514. _this.video.paused = false;
  515. _this.video.player.stopVideo();
  516. }
  517. if (this.playerReady) {
  518. _this
  519. .getCurrentTime(3)
  520. .then(time => (this.youtubeVideoCurrentTime = time));
  521. }
  522. }, 200);
  523. this.video.player = new window.YT.Player("player", {
  524. height: 315,
  525. width: 560,
  526. videoId: this.editing.song.songId,
  527. playerVars: {
  528. controls: 0,
  529. iv_load_policy: 3,
  530. rel: 0,
  531. showinfo: 0,
  532. autoplay: 1
  533. },
  534. startSeconds: _this.editing.song.skipDuration,
  535. events: {
  536. onReady: () => {
  537. let volume = parseInt(localStorage.getItem("volume"));
  538. volume = typeof volume === "number" ? volume : 20;
  539. console.log("Seekto: " + _this.editing.song.skipDuration);
  540. _this.video.player.seekTo(_this.editing.song.skipDuration);
  541. _this.video.player.setVolume(volume);
  542. if (volume > 0) _this.video.player.unMute();
  543. this.youtubeVideoDuration = _this.video.player.getDuration();
  544. this.youtubeVideoNote = "(~)";
  545. _this.playerReady = true;
  546. },
  547. onStateChange: event => {
  548. if (event.data === 1) {
  549. if (!_this.video.autoPlayed) {
  550. _this.video.autoPlayed = true;
  551. return _this.video.player.stopVideo();
  552. }
  553. _this.video.paused = false;
  554. let youtubeDuration = _this.video.player.getDuration();
  555. this.youtubeVideoDuration = youtubeDuration;
  556. this.youtubeVideoNote = "";
  557. youtubeDuration -= _this.editing.song.skipDuration;
  558. if (_this.editing.song.duration > youtubeDuration + 1) {
  559. this.video.player.stopVideo();
  560. _this.video.paused = true;
  561. Toast.methods.addToast(
  562. "Video can't play. Specified duration is bigger than the YouTube song duration.",
  563. 4000
  564. );
  565. } else if (_this.editing.song.duration <= 0) {
  566. this.video.player.stopVideo();
  567. _this.video.paused = true;
  568. Toast.methods.addToast(
  569. "Video can't play. Specified duration has to be more than 0 seconds.",
  570. 4000
  571. );
  572. }
  573. if (
  574. _this.getCurrentTime(time => {
  575. return time;
  576. }) < _this.editing.song.skipDuration
  577. ) {
  578. _this.video.player.seekTo(
  579. _this.editing.song.skipDuration
  580. );
  581. }
  582. } else if (event.data === 2) {
  583. this.video.paused = true;
  584. }
  585. }
  586. }
  587. });
  588. let volume = parseInt(localStorage.getItem("volume"));
  589. document.getElementById("volumeSlider").value = volume =
  590. typeof volume === "number" ? volume : 20;
  591. }
  592. };
  593. </script>
  594. <style lang="scss" scoped>
  595. input[type="range"] {
  596. -webkit-appearance: none;
  597. width: 100%;
  598. margin: 7.3px 0;
  599. }
  600. input[type="range"]:focus {
  601. outline: none;
  602. }
  603. input[type="range"]::-webkit-slider-runnable-track {
  604. width: 100%;
  605. height: 5.2px;
  606. cursor: pointer;
  607. box-shadow: 0;
  608. background: #c2c0c2;
  609. border-radius: 0;
  610. border: 0;
  611. }
  612. input[type="range"]::-webkit-slider-thumb {
  613. box-shadow: 0;
  614. border: 0;
  615. height: 19px;
  616. width: 19px;
  617. border-radius: 15px;
  618. background: #03a9f4;
  619. cursor: pointer;
  620. -webkit-appearance: none;
  621. margin-top: -6.5px;
  622. }
  623. input[type="range"]::-moz-range-track {
  624. width: 100%;
  625. height: 5.2px;
  626. cursor: pointer;
  627. box-shadow: 0;
  628. background: #c2c0c2;
  629. border-radius: 0;
  630. border: 0;
  631. }
  632. input[type="range"]::-moz-range-thumb {
  633. box-shadow: 0;
  634. border: 0;
  635. height: 19px;
  636. width: 19px;
  637. border-radius: 15px;
  638. background: #03a9f4;
  639. cursor: pointer;
  640. -webkit-appearance: none;
  641. margin-top: -6.5px;
  642. }
  643. input[type="range"]::-ms-track {
  644. width: 100%;
  645. height: 5.2px;
  646. cursor: pointer;
  647. box-shadow: 0;
  648. background: #c2c0c2;
  649. border-radius: 1.3px;
  650. }
  651. input[type="range"]::-ms-fill-lower {
  652. background: #c2c0c2;
  653. border: 0;
  654. border-radius: 0;
  655. box-shadow: 0;
  656. }
  657. input[type="range"]::-ms-fill-upper {
  658. background: #c2c0c2;
  659. border: 0;
  660. border-radius: 0;
  661. box-shadow: 0;
  662. }
  663. input[type="range"]::-ms-thumb {
  664. box-shadow: 0;
  665. border: 0;
  666. height: 15px;
  667. width: 15px;
  668. border-radius: 15px;
  669. background: #03a9f4;
  670. cursor: pointer;
  671. -webkit-appearance: none;
  672. margin-top: 1.5px;
  673. }
  674. .controls {
  675. display: flex;
  676. flex-direction: column;
  677. align-items: center;
  678. }
  679. .artist-genres {
  680. display: flex;
  681. justify-content: space-between;
  682. }
  683. #volumeSlider {
  684. margin-bottom: 15px;
  685. }
  686. .has-text-centered {
  687. padding: 10px;
  688. }
  689. .thumbnail-preview {
  690. display: flex;
  691. margin: 0 auto 25px auto;
  692. max-width: 200px;
  693. width: 100%;
  694. }
  695. .modal-card-body,
  696. .modal-card-foot {
  697. border-top: 0;
  698. }
  699. .label,
  700. .checkbox,
  701. h5 {
  702. font-weight: normal;
  703. }
  704. .video-container {
  705. display: flex;
  706. flex-direction: column;
  707. align-items: center;
  708. padding: 10px;
  709. iframe {
  710. pointer-events: none;
  711. }
  712. }
  713. .save-changes {
  714. color: #fff;
  715. }
  716. .tag:not(:last-child) {
  717. margin-right: 5px;
  718. }
  719. .reports-length {
  720. color: #ff4545;
  721. font-weight: bold;
  722. display: flex;
  723. justify-content: center;
  724. }
  725. .report-link {
  726. color: #000;
  727. }
  728. </style>