EditSong.vue 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776
  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.getCurrentTime().then(time => {
  511. return time;
  512. }) -
  513. _this.editing.song.skipDuration >
  514. _this.editing.song.duration
  515. ) {
  516. _this.video.paused = false;
  517. _this.video.player.stopVideo();
  518. }
  519. if (this.playerReady) {
  520. _this
  521. .getCurrentTime(3)
  522. .then(time => (this.youtubeVideoCurrentTime = time));
  523. }
  524. }, 200);
  525. this.video.player = new window.YT.Player("player", {
  526. height: 315,
  527. width: 560,
  528. videoId: this.editing.song.songId,
  529. playerVars: {
  530. controls: 0,
  531. iv_load_policy: 3,
  532. rel: 0,
  533. showinfo: 0,
  534. autoplay: 1
  535. },
  536. startSeconds: _this.editing.song.skipDuration,
  537. events: {
  538. onReady: () => {
  539. let volume = parseInt(localStorage.getItem("volume"));
  540. volume = typeof volume === "number" ? volume : 20;
  541. console.log("Seekto: " + _this.editing.song.skipDuration);
  542. _this.video.player.seekTo(_this.editing.song.skipDuration);
  543. _this.video.player.setVolume(volume);
  544. if (volume > 0) _this.video.player.unMute();
  545. this.youtubeVideoDuration = _this.video.player.getDuration();
  546. this.youtubeVideoNote = "(~)";
  547. _this.playerReady = true;
  548. },
  549. onStateChange: event => {
  550. if (event.data === 1) {
  551. if (!_this.video.autoPlayed) {
  552. _this.video.autoPlayed = true;
  553. return _this.video.player.stopVideo();
  554. }
  555. _this.video.paused = false;
  556. let youtubeDuration = _this.video.player.getDuration();
  557. this.youtubeVideoDuration = youtubeDuration;
  558. this.youtubeVideoNote = "";
  559. youtubeDuration -= _this.editing.song.skipDuration;
  560. if (_this.editing.song.duration > youtubeDuration) {
  561. this.video.player.stopVideo();
  562. _this.video.paused = true;
  563. Toast.methods.addToast(
  564. "Video can't play. Specified duration is bigger than the YouTube song duration.",
  565. 4000
  566. );
  567. } else if (_this.editing.song.duration <= 0) {
  568. this.video.player.stopVideo();
  569. _this.video.paused = true;
  570. Toast.methods.addToast(
  571. "Video can't play. Specified duration has to be more than 0 seconds.",
  572. 4000
  573. );
  574. }
  575. if (
  576. _this.getCurrentTime(time => {
  577. return time;
  578. }) < _this.editing.song.skipDuration
  579. ) {
  580. _this.video.player.seekTo(
  581. _this.editing.song.skipDuration
  582. );
  583. }
  584. } else if (event.data === 2) {
  585. this.video.paused = true;
  586. }
  587. }
  588. }
  589. });
  590. let volume = parseInt(localStorage.getItem("volume"));
  591. document.getElementById("volumeSlider").value = volume =
  592. typeof volume === "number" ? volume : 20;
  593. }
  594. };
  595. </script>
  596. <style lang="scss" scoped>
  597. input[type="range"] {
  598. -webkit-appearance: none;
  599. width: 100%;
  600. margin: 7.3px 0;
  601. }
  602. input[type="range"]:focus {
  603. outline: none;
  604. }
  605. input[type="range"]::-webkit-slider-runnable-track {
  606. width: 100%;
  607. height: 5.2px;
  608. cursor: pointer;
  609. box-shadow: 0;
  610. background: #c2c0c2;
  611. border-radius: 0;
  612. border: 0;
  613. }
  614. input[type="range"]::-webkit-slider-thumb {
  615. box-shadow: 0;
  616. border: 0;
  617. height: 19px;
  618. width: 19px;
  619. border-radius: 15px;
  620. background: #03a9f4;
  621. cursor: pointer;
  622. -webkit-appearance: none;
  623. margin-top: -6.5px;
  624. }
  625. input[type="range"]::-moz-range-track {
  626. width: 100%;
  627. height: 5.2px;
  628. cursor: pointer;
  629. box-shadow: 0;
  630. background: #c2c0c2;
  631. border-radius: 0;
  632. border: 0;
  633. }
  634. input[type="range"]::-moz-range-thumb {
  635. box-shadow: 0;
  636. border: 0;
  637. height: 19px;
  638. width: 19px;
  639. border-radius: 15px;
  640. background: #03a9f4;
  641. cursor: pointer;
  642. -webkit-appearance: none;
  643. margin-top: -6.5px;
  644. }
  645. input[type="range"]::-ms-track {
  646. width: 100%;
  647. height: 5.2px;
  648. cursor: pointer;
  649. box-shadow: 0;
  650. background: #c2c0c2;
  651. border-radius: 1.3px;
  652. }
  653. input[type="range"]::-ms-fill-lower {
  654. background: #c2c0c2;
  655. border: 0;
  656. border-radius: 0;
  657. box-shadow: 0;
  658. }
  659. input[type="range"]::-ms-fill-upper {
  660. background: #c2c0c2;
  661. border: 0;
  662. border-radius: 0;
  663. box-shadow: 0;
  664. }
  665. input[type="range"]::-ms-thumb {
  666. box-shadow: 0;
  667. border: 0;
  668. height: 15px;
  669. width: 15px;
  670. border-radius: 15px;
  671. background: #03a9f4;
  672. cursor: pointer;
  673. -webkit-appearance: none;
  674. margin-top: 1.5px;
  675. }
  676. .controls {
  677. display: flex;
  678. flex-direction: column;
  679. align-items: center;
  680. }
  681. .artist-genres {
  682. display: flex;
  683. justify-content: space-between;
  684. }
  685. #volumeSlider {
  686. margin-bottom: 15px;
  687. }
  688. .has-text-centered {
  689. padding: 10px;
  690. }
  691. .thumbnail-preview {
  692. display: flex;
  693. margin: 0 auto 25px auto;
  694. max-width: 200px;
  695. width: 100%;
  696. }
  697. .modal-card-body,
  698. .modal-card-foot {
  699. border-top: 0;
  700. }
  701. .label,
  702. .checkbox,
  703. h5 {
  704. font-weight: normal;
  705. }
  706. .video-container {
  707. display: flex;
  708. flex-direction: column;
  709. align-items: center;
  710. padding: 10px;
  711. iframe {
  712. pointer-events: none;
  713. }
  714. }
  715. .save-changes {
  716. color: #fff;
  717. }
  718. .tag:not(:last-child) {
  719. margin-right: 5px;
  720. }
  721. .reports-length {
  722. color: #ff4545;
  723. font-weight: bold;
  724. display: flex;
  725. justify-content: center;
  726. }
  727. .report-link {
  728. color: #000;
  729. }
  730. </style>