index.vue 53 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263
  1. <template>
  2. <div>
  3. <modal
  4. :title="`${newSong ? 'Create' : 'Edit'} Song`"
  5. class="song-modal"
  6. :size="'wide'"
  7. :split="true"
  8. :intercept-close="true"
  9. @close="onCloseModal"
  10. >
  11. <template #toggleMobileSidebar>
  12. <slot name="toggleMobileSidebar" />
  13. </template>
  14. <template #sidebar>
  15. <slot name="sidebar" />
  16. </template>
  17. <template #body>
  18. <div v-if="!songId && !newSong" class="notice-container">
  19. <h4>No song has been selected</h4>
  20. </div>
  21. <div
  22. v-if="
  23. songId && !songDataLoaded && !songNotFound && !newSong
  24. "
  25. class="notice-container"
  26. >
  27. <h4>Song hasn't loaded yet</h4>
  28. </div>
  29. <div
  30. v-if="songId && songNotFound && !newSong"
  31. class="notice-container"
  32. >
  33. <h4>Song was not found</h4>
  34. </div>
  35. <div class="left-section" v-show="songDataLoaded">
  36. <div class="top-section">
  37. <div class="player-section">
  38. <div id="editSongPlayer" />
  39. <div v-show="youtubeError" class="player-error">
  40. <h2>{{ youtubeErrorMessage }}</h2>
  41. </div>
  42. <canvas
  43. ref="durationCanvas"
  44. id="durationCanvas"
  45. v-show="!youtubeError"
  46. height="20"
  47. width="530"
  48. @click="setTrackPosition($event)"
  49. />
  50. <div id="playerTrack">
  51. <div class="skip-duration"></div>
  52. <div class="real-duration"></div>
  53. </div>
  54. <div class="player-footer">
  55. <div class="player-footer-left">
  56. <button
  57. class="button is-primary"
  58. @click="play()"
  59. @keyup.enter="play()"
  60. v-if="video.paused"
  61. content="Unpause Playback"
  62. v-tippy
  63. >
  64. <i class="material-icons">play_arrow</i>
  65. </button>
  66. <button
  67. class="button is-primary"
  68. @click="settings('pause')"
  69. @keyup.enter="settings('pause')"
  70. v-else
  71. content="Pause Playback"
  72. v-tippy
  73. >
  74. <i class="material-icons">pause</i>
  75. </button>
  76. <button
  77. class="button is-danger"
  78. @click="settings('stop')"
  79. @keyup.enter="settings('stop')"
  80. content="Stop Playback"
  81. v-tippy
  82. >
  83. <i class="material-icons">stop</i>
  84. </button>
  85. <button
  86. class="button is-success"
  87. @click="settings('skipToLast10Secs')"
  88. @keyup.enter="
  89. settings('skipToLast10Secs')
  90. "
  91. content="Skip to last 10 secs"
  92. v-tippy
  93. >
  94. <i class="material-icons"
  95. >fast_forward</i
  96. >
  97. </button>
  98. </div>
  99. <div class="player-footer-center">
  100. <span>
  101. <span>
  102. {{ youtubeVideoCurrentTime }}
  103. </span>
  104. /
  105. <span>
  106. {{ youtubeVideoDuration }}
  107. {{ youtubeVideoNote }}
  108. </span>
  109. </span>
  110. </div>
  111. <div class="player-footer-right">
  112. <p id="volume-control">
  113. <i
  114. v-if="muted"
  115. class="material-icons"
  116. @click="toggleMute()"
  117. content="Unmute"
  118. v-tippy
  119. >volume_mute</i
  120. >
  121. <i
  122. v-else
  123. class="material-icons"
  124. @click="toggleMute()"
  125. content="Mute"
  126. v-tippy
  127. >volume_down</i
  128. >
  129. <input
  130. v-model="volumeSliderValue"
  131. type="range"
  132. min="0"
  133. max="10000"
  134. class="volume-slider active"
  135. @change="changeVolume()"
  136. @input="changeVolume()"
  137. />
  138. <i
  139. class="material-icons"
  140. @click="increaseVolume()"
  141. content="Increase Volume"
  142. v-tippy
  143. >volume_up</i
  144. >
  145. </p>
  146. </div>
  147. </div>
  148. </div>
  149. <img
  150. class="thumbnail-preview"
  151. :src="song.thumbnail"
  152. onerror="this.src='/assets/notes-transparent.png'"
  153. ref="thumbnailElement"
  154. v-if="songDataLoaded"
  155. />
  156. </div>
  157. <div class="edit-section" v-if="songDataLoaded">
  158. <div class="control is-grouped">
  159. <div class="title-container">
  160. <label class="label">Title</label>
  161. <p class="control has-addons">
  162. <input
  163. class="input"
  164. type="text"
  165. ref="title-input"
  166. v-model="song.title"
  167. placeholder="Enter song title..."
  168. @keyup.shift.enter="
  169. getAlbumData('title')
  170. "
  171. />
  172. <button
  173. class="button album-get-button"
  174. @click="getAlbumData('title')"
  175. >
  176. <i
  177. class="material-icons"
  178. v-tippy
  179. content="Fill from Discogs"
  180. >album</i
  181. >
  182. </button>
  183. </p>
  184. </div>
  185. <div class="duration-container">
  186. <label class="label">Duration</label>
  187. <p class="control has-addons">
  188. <input
  189. class="input"
  190. type="text"
  191. placeholder="Enter song duration..."
  192. v-model.number="song.duration"
  193. @keyup.shift.enter="fillDuration()"
  194. />
  195. <button
  196. class="button duration-fill-button"
  197. @click="fillDuration()"
  198. >
  199. <i
  200. class="material-icons"
  201. v-tippy
  202. content="Sync duration with YouTube"
  203. >sync</i
  204. >
  205. </button>
  206. </p>
  207. </div>
  208. <div class="skip-duration-container">
  209. <label class="label">Skip duration</label>
  210. <p class="control">
  211. <input
  212. class="input"
  213. type="text"
  214. placeholder="Enter skip duration..."
  215. v-model.number="song.skipDuration"
  216. />
  217. </p>
  218. </div>
  219. </div>
  220. <div class="control is-grouped">
  221. <div class="album-art-container">
  222. <label class="label">Album art</label>
  223. <p class="control has-addons">
  224. <input
  225. class="input"
  226. type="text"
  227. v-model="song.thumbnail"
  228. placeholder="Enter link to album art..."
  229. @keyup.shift.enter="
  230. getAlbumData('albumArt')
  231. "
  232. />
  233. <button
  234. class="button album-get-button"
  235. @click="getAlbumData('albumArt')"
  236. >
  237. <i
  238. class="material-icons"
  239. v-tippy
  240. content="Fill from Discogs"
  241. >album</i
  242. >
  243. </button>
  244. </p>
  245. </div>
  246. <div class="youtube-id-container">
  247. <label class="label">YouTube ID</label>
  248. <p class="control">
  249. <input
  250. class="input"
  251. type="text"
  252. placeholder="Enter YouTube ID..."
  253. v-model="song.youtubeId"
  254. />
  255. </p>
  256. </div>
  257. </div>
  258. <div class="control is-grouped">
  259. <div class="artists-container">
  260. <label class="label">Artists</label>
  261. <p class="control has-addons">
  262. <auto-suggest
  263. v-model="artistInputValue"
  264. ref="new-artist"
  265. placeholder="Add artist..."
  266. :all-items="
  267. autosuggest.allItems.artists
  268. "
  269. @submitted="addTag('artists')"
  270. @keyup.shift.enter="
  271. getAlbumData('artists')
  272. "
  273. />
  274. <button
  275. class="button album-get-button"
  276. @click="getAlbumData('artists')"
  277. >
  278. <i
  279. class="material-icons"
  280. v-tippy
  281. content="Fill from Discogs"
  282. >album</i
  283. >
  284. </button>
  285. <button
  286. class="button is-info add-button"
  287. @click="addTag('artists')"
  288. >
  289. <i class="material-icons">add</i>
  290. </button>
  291. </p>
  292. <div class="list-container">
  293. <div
  294. class="list-item"
  295. v-for="artist in song.artists"
  296. :key="artist"
  297. >
  298. <div
  299. class="list-item-circle"
  300. @click="
  301. removeTag('artists', artist)
  302. "
  303. >
  304. <i class="material-icons">close</i>
  305. </div>
  306. <p>{{ artist }}</p>
  307. </div>
  308. </div>
  309. </div>
  310. <div class="genres-container">
  311. <label class="label">
  312. <span>Genres</span>
  313. <i
  314. class="material-icons"
  315. @click="toggleGenreHelper"
  316. @dblclick="resetGenreHelper"
  317. v-tippy
  318. content="View list of genres"
  319. >info</i
  320. >
  321. </label>
  322. <p class="control has-addons">
  323. <auto-suggest
  324. v-model="genreInputValue"
  325. ref="new-genre"
  326. placeholder="Add genre..."
  327. :all-items="autosuggest.allItems.genres"
  328. @submitted="addTag('genres')"
  329. @keyup.shift.enter="
  330. getAlbumData('genres')
  331. "
  332. />
  333. <button
  334. class="button album-get-button"
  335. @click="getAlbumData('genres')"
  336. >
  337. <i
  338. class="material-icons"
  339. v-tippy
  340. content="Fill from Discogs"
  341. >album</i
  342. >
  343. </button>
  344. <button
  345. class="button is-info add-button"
  346. @click="addTag('genres')"
  347. >
  348. <i class="material-icons">add</i>
  349. </button>
  350. </p>
  351. <div class="list-container">
  352. <div
  353. class="list-item"
  354. v-for="genre in song.genres"
  355. :key="genre"
  356. >
  357. <div
  358. class="list-item-circle"
  359. @click="removeTag('genres', genre)"
  360. >
  361. <i class="material-icons">close</i>
  362. </div>
  363. <p>{{ genre }}</p>
  364. </div>
  365. </div>
  366. </div>
  367. <div class="tags-container">
  368. <label class="label">Tags</label>
  369. <p class="control has-addons">
  370. <auto-suggest
  371. v-model="tagInputValue"
  372. ref="new-tag"
  373. placeholder="Add tag..."
  374. :all-items="autosuggest.allItems.tags"
  375. @submitted="addTag('tags')"
  376. />
  377. <button
  378. class="button is-info add-button"
  379. @click="addTag('tags')"
  380. >
  381. <i class="material-icons">add</i>
  382. </button>
  383. </p>
  384. <div class="list-container">
  385. <div
  386. class="list-item"
  387. v-for="tag in song.tags"
  388. :key="tag"
  389. >
  390. <div
  391. class="list-item-circle"
  392. @click="removeTag('tags', tag)"
  393. >
  394. <i class="material-icons">close</i>
  395. </div>
  396. <p>{{ tag }}</p>
  397. </div>
  398. </div>
  399. </div>
  400. </div>
  401. </div>
  402. </div>
  403. <div class="right-section" v-if="songDataLoaded">
  404. <div id="tabs-container">
  405. <div id="tab-selection">
  406. <button
  407. class="button is-default"
  408. :class="{ selected: tab === 'discogs' }"
  409. ref="discogs-tab"
  410. @click="showTab('discogs')"
  411. >
  412. Discogs
  413. </button>
  414. <button
  415. v-if="!newSong"
  416. class="button is-default"
  417. :class="{ selected: tab === 'reports' }"
  418. ref="reports-tab"
  419. @click="showTab('reports')"
  420. >
  421. Reports ({{ reports.length }})
  422. </button>
  423. <button
  424. class="button is-default"
  425. :class="{ selected: tab === 'youtube' }"
  426. ref="youtube-tab"
  427. @click="showTab('youtube')"
  428. >
  429. YouTube
  430. </button>
  431. <button
  432. class="button is-default"
  433. :class="{ selected: tab === 'musare-songs' }"
  434. ref="musare-songs-tab"
  435. @click="showTab('musare-songs')"
  436. >
  437. Songs
  438. </button>
  439. </div>
  440. <discogs
  441. class="tab"
  442. v-show="tab === 'discogs'"
  443. :bulk="bulk"
  444. />
  445. <reports
  446. v-if="!newSong"
  447. class="tab"
  448. v-show="tab === 'reports'"
  449. />
  450. <youtube class="tab" v-show="tab === 'youtube'" />
  451. <musare-songs
  452. class="tab"
  453. v-show="tab === 'musare-songs'"
  454. />
  455. </div>
  456. </div>
  457. </template>
  458. <template #footer>
  459. <div v-if="bulk">
  460. <button class="button is-primary" @click="editNextSong()">
  461. Next
  462. </button>
  463. <button
  464. class="button is-primary"
  465. @click="toggleFlag()"
  466. v-if="songId"
  467. >
  468. {{ flagged ? "Unflag" : "Flag" }}
  469. </button>
  470. </div>
  471. <div v-if="!newSong">
  472. <save-button
  473. ref="saveButton"
  474. @clicked="save(song, false, false, 'saveButton')"
  475. />
  476. <save-button
  477. ref="saveAndCloseButton"
  478. :default-message="
  479. bulk ? `Save and next` : `Save and close`
  480. "
  481. @clicked="save(song, false, true, 'saveAndCloseButton')"
  482. />
  483. <save-button
  484. ref="saveVerifyAndCloseButton"
  485. :default-message="
  486. bulk
  487. ? `Save, verify and next`
  488. : `Save, verify and close`
  489. "
  490. @click="
  491. save(song, true, true, 'saveVerifyAndCloseButton')
  492. "
  493. />
  494. <div class="right">
  495. <button
  496. v-if="!song.verified"
  497. class="button is-success"
  498. @click="verify(song._id)"
  499. content="Verify Song"
  500. v-tippy
  501. >
  502. <i class="material-icons">check_circle</i>
  503. </button>
  504. <quick-confirm
  505. v-else
  506. placement="left"
  507. @confirm="unverify(song._id)"
  508. >
  509. <button
  510. class="button is-danger"
  511. content="Unverify Song"
  512. v-tippy
  513. >
  514. <i class="material-icons">cancel</i>
  515. </button>
  516. </quick-confirm>
  517. <button
  518. class="button is-danger icon-with-button material-icons"
  519. @click.prevent="
  520. confirmAction({
  521. message:
  522. 'Removing this song will remove it from all playlists and cause a ratings recalculation.',
  523. action: 'remove',
  524. params: song._id
  525. })
  526. "
  527. content="Delete Song"
  528. v-tippy
  529. >
  530. delete_forever
  531. </button>
  532. </div>
  533. </div>
  534. <div v-else>
  535. <save-button
  536. ref="createButton"
  537. default-message="Create Song"
  538. @clicked="
  539. save(song, false, false, 'createButton', true)
  540. "
  541. />
  542. <div class="right">
  543. <button
  544. v-if="!song.verified"
  545. class="button is-success"
  546. @click="verify()"
  547. content="Verify Song"
  548. v-tippy
  549. >
  550. <i class="material-icons">check_circle</i>
  551. </button>
  552. <button
  553. v-else
  554. class="button is-danger"
  555. @click="unverify()"
  556. content="Unverify Song"
  557. v-tippy
  558. >
  559. <i class="material-icons">cancel</i>
  560. </button>
  561. </div>
  562. </div>
  563. </template>
  564. </modal>
  565. <floating-box id="genreHelper" ref="genreHelper" :column="false">
  566. <template #body>
  567. <span
  568. v-for="item in autosuggest.allItems.genres"
  569. :key="`genre-helper-${item}`"
  570. >
  571. {{ item }}
  572. </span>
  573. </template>
  574. </floating-box>
  575. <confirm v-if="modals.editSongConfirm" @confirmed="handleConfirmed()" />
  576. </div>
  577. </template>
  578. <script>
  579. import { mapState, mapGetters, mapActions } from "vuex";
  580. import { defineAsyncComponent } from "vue";
  581. import Toast from "toasters";
  582. import aw from "@/aw";
  583. import ws from "@/ws";
  584. import validation from "@/validation";
  585. import keyboardShortcuts from "@/keyboardShortcuts";
  586. import QuickConfirm from "@/components/QuickConfirm.vue";
  587. import Modal from "../../Modal.vue";
  588. import FloatingBox from "../../FloatingBox.vue";
  589. import SaveButton from "../../SaveButton.vue";
  590. import AutoSuggest from "@/components/AutoSuggest.vue";
  591. import Discogs from "./Tabs/Discogs.vue";
  592. import Reports from "./Tabs/Reports.vue";
  593. import Youtube from "./Tabs/Youtube.vue";
  594. import MusareSongs from "./Tabs/Songs.vue";
  595. export default {
  596. components: {
  597. Modal,
  598. FloatingBox,
  599. SaveButton,
  600. QuickConfirm,
  601. AutoSuggest,
  602. Discogs,
  603. Reports,
  604. Youtube,
  605. MusareSongs,
  606. Confirm: defineAsyncComponent(() =>
  607. import("@/components/modals/Confirm.vue")
  608. )
  609. },
  610. props: {
  611. // songId: { type: String, default: null },
  612. discogsAlbum: { type: Object, default: null },
  613. sector: { type: String, default: "admin" },
  614. bulk: { type: Boolean, default: false },
  615. flagged: { type: Boolean, default: false }
  616. },
  617. emits: [
  618. "error",
  619. "savedSuccess",
  620. "savedError",
  621. "flagSong",
  622. "nextSong",
  623. "close"
  624. ],
  625. data() {
  626. return {
  627. songDataLoaded: false,
  628. youtubeError: false,
  629. youtubeErrorMessage: "",
  630. focusedElementBefore: null,
  631. youtubeVideoDuration: "0.000",
  632. youtubeVideoCurrentTime: 0,
  633. youtubeVideoNote: "",
  634. useHTTPS: false,
  635. muted: false,
  636. volumeSliderValue: 0,
  637. skipToLast10SecsPressed: false,
  638. artistInputValue: "",
  639. genreInputValue: "",
  640. tagInputValue: "",
  641. activityWatchVideoDataInterval: null,
  642. activityWatchVideoLastStatus: "",
  643. activityWatchVideoLastStartDuration: "",
  644. confirm: {
  645. message: "",
  646. action: "",
  647. params: null
  648. },
  649. recommendedGenres: [
  650. "Blues",
  651. "Country",
  652. "Disco",
  653. "Funk",
  654. "Hip-Hop",
  655. "Jazz",
  656. "Metal",
  657. "Oldies",
  658. "Other",
  659. "Pop",
  660. "Rap",
  661. "Reggae",
  662. "Rock",
  663. "Techno",
  664. "Trance",
  665. "Classical",
  666. "Instrumental",
  667. "House",
  668. "Electronic",
  669. "Christian Rap",
  670. "Lo-Fi",
  671. "Musical",
  672. "Rock 'n' Roll",
  673. "Opera",
  674. "Drum & Bass",
  675. "Club-House",
  676. "Indie",
  677. "Heavy Metal",
  678. "Christian rock",
  679. "Dubstep"
  680. ],
  681. autosuggest: {
  682. allItems: {
  683. artists: [],
  684. genres: [],
  685. tags: []
  686. }
  687. },
  688. songNotFound: false
  689. };
  690. },
  691. computed: {
  692. ...mapState("modals/editSong", {
  693. tab: state => state.tab,
  694. video: state => state.video,
  695. song: state => state.song,
  696. songId: state => state.songId,
  697. prefillData: state => state.prefillData,
  698. originalSong: state => state.originalSong,
  699. reports: state => state.reports,
  700. newSong: state => state.newSong
  701. }),
  702. ...mapState("modalVisibility", {
  703. modals: state => state.modals,
  704. currentlyActive: state => state.currentlyActive
  705. }),
  706. ...mapGetters({
  707. socket: "websockets/getSocket"
  708. })
  709. },
  710. watch: {
  711. /* eslint-disable */
  712. "song.duration": function () {
  713. this.drawCanvas();
  714. },
  715. "song.skipDuration": function () {
  716. this.drawCanvas();
  717. },
  718. /* eslint-enable */
  719. songId(songId, oldSongId) {
  720. console.log("NEW SONG ID", songId);
  721. this.unloadSong(oldSongId);
  722. this.loadSong(songId);
  723. }
  724. },
  725. async mounted() {
  726. console.log("MOUNTED");
  727. this.activityWatchVideoDataInterval = setInterval(() => {
  728. this.sendActivityWatchVideoData();
  729. }, 1000);
  730. this.useHTTPS = await lofig.get("cookie.secure");
  731. ws.onConnect(this.init);
  732. let volume = parseFloat(localStorage.getItem("volume"));
  733. volume =
  734. typeof volume === "number" && !Number.isNaN(volume) ? volume : 20;
  735. localStorage.setItem("volume", volume);
  736. this.volumeSliderValue = volume * 100;
  737. if (!this.newSong) {
  738. this.socket.on(
  739. "event:admin.song.updated",
  740. res => {
  741. if (res.data.song._id === this.song._id)
  742. this.song.verified = res.data.song.verified;
  743. },
  744. { modal: "editSong" }
  745. );
  746. this.socket.on(
  747. "event:admin.song.removed",
  748. res => {
  749. if (res.data.songId === this.song._id) {
  750. this.closeModal("editSong");
  751. setTimeout(() => {
  752. window.focusedElementBefore.focus();
  753. }, 500);
  754. }
  755. },
  756. { modal: "editSong" }
  757. );
  758. }
  759. keyboardShortcuts.registerShortcut("editSong.pauseResumeVideo", {
  760. keyCode: 101,
  761. preventDefault: true,
  762. handler: () => {
  763. if (this.video.paused) this.play();
  764. else this.settings("pause");
  765. }
  766. });
  767. keyboardShortcuts.registerShortcut("editSong.stopVideo", {
  768. keyCode: 101,
  769. ctrl: true,
  770. preventDefault: true,
  771. handler: () => {
  772. this.settings("stop");
  773. }
  774. });
  775. keyboardShortcuts.registerShortcut("editSong.skipToLast10Secs", {
  776. keyCode: 102,
  777. preventDefault: true,
  778. handler: () => {
  779. this.settings("skipToLast10Secs");
  780. }
  781. });
  782. keyboardShortcuts.registerShortcut("editSong.lowerVolumeLarge", {
  783. keyCode: 98,
  784. preventDefault: true,
  785. handler: () => {
  786. this.volumeSliderValue = Math.max(
  787. 0,
  788. this.volumeSliderValue - 1000
  789. );
  790. this.changeVolume();
  791. }
  792. });
  793. keyboardShortcuts.registerShortcut("editSong.lowerVolumeSmall", {
  794. keyCode: 98,
  795. ctrl: true,
  796. preventDefault: true,
  797. handler: () => {
  798. this.volumeSliderValue = Math.max(
  799. 0,
  800. this.volumeSliderValue - 100
  801. );
  802. this.changeVolume();
  803. }
  804. });
  805. keyboardShortcuts.registerShortcut("editSong.increaseVolumeLarge", {
  806. keyCode: 104,
  807. preventDefault: true,
  808. handler: () => {
  809. this.volumeSliderValue = Math.min(
  810. 10000,
  811. this.volumeSliderValue + 1000
  812. );
  813. this.changeVolume();
  814. }
  815. });
  816. keyboardShortcuts.registerShortcut("editSong.increaseVolumeSmall", {
  817. keyCode: 104,
  818. ctrl: true,
  819. preventDefault: true,
  820. handler: () => {
  821. this.volumeSliderValue = Math.min(
  822. 10000,
  823. this.volumeSliderValue + 100
  824. );
  825. this.changeVolume();
  826. }
  827. });
  828. keyboardShortcuts.registerShortcut("editSong.save", {
  829. keyCode: 83,
  830. ctrl: true,
  831. preventDefault: true,
  832. handler: () => {
  833. this.save(this.song, false, false, "saveButton");
  834. }
  835. });
  836. keyboardShortcuts.registerShortcut("editSong.saveClose", {
  837. keyCode: 83,
  838. ctrl: true,
  839. alt: true,
  840. preventDefault: true,
  841. handler: () => {
  842. this.save(this.song, false, true, "saveAndCloseButton");
  843. }
  844. });
  845. // TODO
  846. keyboardShortcuts.registerShortcut("editSong.saveVerifyClose", {
  847. keyCode: 86,
  848. ctrl: true,
  849. alt: true,
  850. preventDefault: true,
  851. handler: () => {
  852. // alert("not implemented yet");
  853. this.save(this.song, true, true, "saveVerifyAndCloseButton");
  854. }
  855. });
  856. keyboardShortcuts.registerShortcut("editSong.focusTitle", {
  857. keyCode: 36,
  858. preventDefault: true,
  859. handler: () => {
  860. this.$refs["title-input"].focus();
  861. }
  862. });
  863. keyboardShortcuts.registerShortcut("editSong.useAllDiscogs", {
  864. keyCode: 68,
  865. alt: true,
  866. ctrl: true,
  867. preventDefault: true,
  868. handler: () => {
  869. this.getAlbumData("title");
  870. this.getAlbumData("albumArt");
  871. this.getAlbumData("artists");
  872. this.getAlbumData("genres");
  873. }
  874. });
  875. keyboardShortcuts.registerShortcut("editSong.closeModal", {
  876. keyCode: 27,
  877. handler: () => {
  878. if (
  879. this.currentlyActive[0] === "editSong" ||
  880. this.currentlyActive[0] === "editSongs"
  881. ) {
  882. this.onCloseModal();
  883. }
  884. }
  885. });
  886. /*
  887. editSong.pauseResume - Num 5 - Pause/resume song
  888. editSong.stopVideo - Ctrl - Num 5 - Stop
  889. editSong.skipToLast10Secs - Num 6 - Skip to last 10 seconds
  890. editSong.lowerVolumeLarge - Num 2 - Volume down by 10
  891. editSong.lowerVolumeSmall - Ctrl - Num 2 - Volume down by 1
  892. editSong.increaseVolumeLarge - Num 8 - Volume up by 10
  893. editSong.increaseVolumeSmall - Ctrl - Num 8 - Volume up by 1
  894. editSong.focusTitle - Home - Focus the title input
  895. editSong.focusDicogs - End - Focus the discogs input
  896. editSong.save - Ctrl - S - Saves song
  897. editSong.save - Ctrl - Alt - S - Saves song and closes the modal
  898. editSong.save - Ctrl - Alt - V - Saves song, verifies songs and then closes the modal
  899. editSong.close - F4 - Closes modal without saving
  900. editSong.useAllDiscogs - Ctrl - Alt - D - Sets all fields to the Discogs data
  901. Inside Discogs inputs: Ctrl - D - Sets this field to the Discogs data
  902. */
  903. },
  904. beforeUnmount() {
  905. console.log("UNMOUNT");
  906. if (!this.newSong) this.unloadSong(this.songId);
  907. this.playerReady = false;
  908. clearInterval(this.interval);
  909. clearInterval(this.activityWatchVideoDataInterval);
  910. const shortcutNames = [
  911. "editSong.pauseResume",
  912. "editSong.stopVideo",
  913. "editSong.skipToLast10Secs",
  914. "editSong.lowerVolumeLarge",
  915. "editSong.lowerVolumeSmall",
  916. "editSong.increaseVolumeLarge",
  917. "editSong.increaseVolumeSmall",
  918. "editSong.focusTitle",
  919. "editSong.focusDicogs",
  920. "editSong.save",
  921. "editSong.saveClose",
  922. "editSong.saveVerifyClose",
  923. "editSong.useAllDiscogs",
  924. "editSong.closeModal"
  925. ];
  926. shortcutNames.forEach(shortcutName => {
  927. keyboardShortcuts.unregisterShortcut(shortcutName);
  928. });
  929. },
  930. methods: {
  931. init() {
  932. if (this.newSong) {
  933. this.setSong({
  934. youtubeId: "",
  935. title: "",
  936. artists: [],
  937. genres: [],
  938. tags: [],
  939. duration: 0,
  940. skipDuration: 0,
  941. thumbnail: "",
  942. verified: false
  943. });
  944. this.songDataLoaded = true;
  945. } else if (this.songId) this.loadSong(this.songId);
  946. else if (!this.bulk) {
  947. new Toast("You can't open EditSong without editing a song");
  948. return this.closeModal("editSong");
  949. }
  950. this.interval = setInterval(() => {
  951. if (
  952. this.song.duration !== -1 &&
  953. this.video.paused === false &&
  954. this.playerReady &&
  955. (this.video.player.getCurrentTime() -
  956. this.song.skipDuration >
  957. this.song.duration ||
  958. (this.video.player.getCurrentTime() > 0 &&
  959. this.video.player.getCurrentTime() >=
  960. this.video.player.getDuration()))
  961. ) {
  962. this.video.paused = true;
  963. this.video.player.stopVideo();
  964. this.drawCanvas();
  965. }
  966. if (
  967. this.playerReady &&
  968. this.video.player.getVideoData &&
  969. this.video.player.getVideoData().video_id ===
  970. this.song.youtubeId
  971. ) {
  972. const currentTime = this.video.player.getCurrentTime();
  973. if (currentTime !== undefined)
  974. this.youtubeVideoCurrentTime = currentTime.toFixed(3);
  975. if (this.youtubeVideoDuration === "0.000") {
  976. const duration = this.video.player.getDuration();
  977. if (duration !== undefined) {
  978. this.youtubeVideoDuration = duration.toFixed(3);
  979. this.youtubeVideoNote = "(~)";
  980. this.drawCanvas();
  981. }
  982. }
  983. }
  984. if (this.video.paused === false) this.drawCanvas();
  985. }, 200);
  986. if (window.YT && window.YT.Player) {
  987. this.video.player = new window.YT.Player("editSongPlayer", {
  988. height: 298,
  989. width: 530,
  990. videoId: null,
  991. host: "https://www.youtube-nocookie.com",
  992. playerVars: {
  993. controls: 0,
  994. iv_load_policy: 3,
  995. rel: 0,
  996. showinfo: 0,
  997. autoplay: 0
  998. },
  999. startSeconds: this.song.skipDuration,
  1000. events: {
  1001. onReady: () => {
  1002. let volume = parseInt(
  1003. localStorage.getItem("volume")
  1004. );
  1005. volume = typeof volume === "number" ? volume : 20;
  1006. this.video.player.setVolume(volume);
  1007. if (volume > 0) this.video.player.unMute();
  1008. this.playerReady = true;
  1009. if (this.song && this.song._id)
  1010. this.video.player.cueVideoById(
  1011. this.song.youtubeId,
  1012. this.song.skipDuration
  1013. );
  1014. this.drawCanvas();
  1015. },
  1016. onStateChange: event => {
  1017. this.drawCanvas();
  1018. let skipToLast10SecsPressed = false;
  1019. if (
  1020. event.data === 1 &&
  1021. this.skipToLast10SecsPressed
  1022. ) {
  1023. this.skipToLast10SecsPressed = false;
  1024. skipToLast10SecsPressed = true;
  1025. }
  1026. if (event.data === 1 && !skipToLast10SecsPressed) {
  1027. this.video.paused = false;
  1028. let youtubeDuration =
  1029. this.video.player.getDuration();
  1030. const newYoutubeVideoDuration =
  1031. youtubeDuration.toFixed(3);
  1032. const songDurationNumber = Number(
  1033. this.song.duration
  1034. );
  1035. const songDurationNumber2 =
  1036. Number(this.song.duration) + 1;
  1037. const songDurationNumber3 =
  1038. Number(this.song.duration) - 1;
  1039. const fixedSongDuration =
  1040. songDurationNumber.toFixed(3);
  1041. const fixedSongDuration2 =
  1042. songDurationNumber2.toFixed(3);
  1043. const fixedSongDuration3 =
  1044. songDurationNumber3.toFixed(3);
  1045. if (
  1046. this.youtubeVideoDuration !==
  1047. newYoutubeVideoDuration &&
  1048. (fixedSongDuration ===
  1049. this.youtubeVideoDuration ||
  1050. fixedSongDuration2 ===
  1051. this.youtubeVideoDuration ||
  1052. fixedSongDuration3 ===
  1053. this.youtubeVideoDuration)
  1054. )
  1055. this.song.duration =
  1056. newYoutubeVideoDuration;
  1057. this.youtubeVideoDuration =
  1058. newYoutubeVideoDuration;
  1059. this.youtubeVideoNote = "";
  1060. if (this.song.duration === -1)
  1061. this.song.duration = youtubeDuration;
  1062. youtubeDuration -= this.song.skipDuration;
  1063. if (this.song.duration > youtubeDuration + 1) {
  1064. this.video.player.stopVideo();
  1065. this.video.paused = true;
  1066. return new Toast(
  1067. "Video can't play. Specified duration is bigger than the YouTube song duration."
  1068. );
  1069. }
  1070. if (this.song.duration <= 0) {
  1071. this.video.player.stopVideo();
  1072. this.video.paused = true;
  1073. return new Toast(
  1074. "Video can't play. Specified duration has to be more than 0 seconds."
  1075. );
  1076. }
  1077. if (
  1078. this.video.player.getCurrentTime() <
  1079. this.song.skipDuration
  1080. ) {
  1081. return this.seekTo(this.song.skipDuration);
  1082. }
  1083. } else if (event.data === 2) {
  1084. this.video.paused = true;
  1085. }
  1086. return false;
  1087. }
  1088. }
  1089. });
  1090. } else {
  1091. this.youtubeError = true;
  1092. this.youtubeErrorMessage = "Player could not be loaded.";
  1093. }
  1094. ["artists", "genres", "tags"].forEach(type => {
  1095. this.socket.dispatch(
  1096. `songs.get${type.charAt(0).toUpperCase()}${type.slice(1)}`,
  1097. res => {
  1098. if (res.status === "success") {
  1099. const { items } = res.data;
  1100. if (type === "genres")
  1101. this.autosuggest.allItems[type] = Array.from(
  1102. new Set([
  1103. ...this.recommendedGenres,
  1104. ...items
  1105. ])
  1106. );
  1107. else this.autosuggest.allItems[type] = items;
  1108. } else {
  1109. new Toast(res.message);
  1110. }
  1111. }
  1112. );
  1113. });
  1114. return null;
  1115. },
  1116. unloadSong(songId) {
  1117. this.songDataLoaded = false;
  1118. if (this.video.player && this.video.player.stopVideo)
  1119. this.video.player.stopVideo();
  1120. this.resetSong(songId);
  1121. this.youtubeVideoCurrentTime = "0.000";
  1122. this.youtubeVideoDuration = "0.000";
  1123. this.socket.dispatch("apis.leaveRoom", `edit-song.${songId}`);
  1124. if (this.$refs.saveButton) this.$refs.saveButton.status = "default";
  1125. },
  1126. loadSong(songId) {
  1127. console.log(`LOAD SONG ${songId}`);
  1128. this.songNotFound = false;
  1129. this.socket.dispatch(`songs.getSongFromSongId`, songId, res => {
  1130. if (res.status === "success") {
  1131. let { song } = res.data;
  1132. song = Object.assign(song, this.prefillData);
  1133. this.setSong(song);
  1134. this.songDataLoaded = true;
  1135. this.socket.dispatch(
  1136. "apis.joinRoom",
  1137. `edit-song.${this.song._id}`
  1138. );
  1139. if (this.video.player && this.video.player.cueVideoById) {
  1140. this.video.player.cueVideoById(
  1141. this.song.youtubeId,
  1142. this.song.skipDuration
  1143. );
  1144. }
  1145. } else {
  1146. new Toast("Song with that ID not found");
  1147. if (this.bulk) this.songNotFound = true;
  1148. if (!this.bulk) this.closeModal("editSong");
  1149. }
  1150. });
  1151. this.socket.dispatch(
  1152. "reports.getReportsForSong",
  1153. this.song._id,
  1154. res => {
  1155. this.updateReports(res.data.reports);
  1156. }
  1157. );
  1158. },
  1159. importAlbum(result) {
  1160. this.selectDiscogsAlbum(result);
  1161. this.openModal("importAlbum");
  1162. this.closeModal("editSong");
  1163. },
  1164. save(
  1165. songToCopy,
  1166. verify,
  1167. closeOrNext,
  1168. saveButtonRefName,
  1169. newSong = false
  1170. ) {
  1171. const song = JSON.parse(JSON.stringify(songToCopy));
  1172. if (!newSong) this.$emit("saving", song._id);
  1173. const saveButtonRef = this.$refs[saveButtonRefName];
  1174. if (!this.youtubeError && this.youtubeVideoDuration === "0.000") {
  1175. saveButtonRef.handleFailedSave();
  1176. if (!newSong) this.$emit("savedError", song._id);
  1177. return new Toast("The video appears to not be working.");
  1178. }
  1179. if (!song.title) {
  1180. saveButtonRef.handleFailedSave();
  1181. if (!newSong) this.$emit("savedError", song._id);
  1182. return new Toast("Please fill in all fields");
  1183. }
  1184. if (!song.thumbnail) {
  1185. saveButtonRef.handleFailedSave();
  1186. if (!newSong) this.$emit("savedError", song._id);
  1187. return new Toast("Please fill in all fields");
  1188. }
  1189. // const thumbnailHeight = this.$refs.thumbnailElement.naturalHeight;
  1190. // const thumbnailWidth = this.$refs.thumbnailElement.naturalWidth;
  1191. // if (thumbnailHeight < 80 || thumbnailWidth < 80) {
  1192. // saveButtonRef.handleFailedSave();
  1193. // return new Toast(
  1194. // "Thumbnail width and height must be at least 80px."
  1195. // );
  1196. // }
  1197. // if (thumbnailHeight > 4000 || thumbnailWidth > 4000) {
  1198. // saveButtonRef.handleFailedSave();
  1199. // return new Toast(
  1200. // "Thumbnail width and height must be less than 4000px."
  1201. // );
  1202. // }
  1203. // if (thumbnailHeight - thumbnailWidth > 5) {
  1204. // saveButtonRef.handleFailedSave();
  1205. // return new Toast("Thumbnail cannot be taller than it is wide.");
  1206. // }
  1207. // Youtube Id
  1208. if (
  1209. !newSong &&
  1210. this.youtubeError &&
  1211. this.originalSong.youtubeId !== song.youtubeId
  1212. ) {
  1213. saveButtonRef.handleFailedSave();
  1214. if (!newSong) this.$emit("savedError", song._id);
  1215. return new Toast(
  1216. "You're not allowed to change the YouTube id while the player is not working"
  1217. );
  1218. }
  1219. // Duration
  1220. if (
  1221. Number(song.skipDuration) + Number(song.duration) >
  1222. this.youtubeVideoDuration &&
  1223. ((!newSong && !this.youtubeError) ||
  1224. this.originalSong.duration !== song.duration)
  1225. ) {
  1226. saveButtonRef.handleFailedSave();
  1227. if (!newSong) this.$emit("savedError", song._id);
  1228. return new Toast(
  1229. "Duration can't be higher than the length of the video"
  1230. );
  1231. }
  1232. // Title
  1233. if (!validation.isLength(song.title, 1, 100)) {
  1234. saveButtonRef.handleFailedSave();
  1235. if (!newSong) this.$emit("savedError", song._id);
  1236. return new Toast(
  1237. "Title must have between 1 and 100 characters."
  1238. );
  1239. }
  1240. // Artists
  1241. if (song.artists.length < 1 || song.artists.length > 10) {
  1242. saveButtonRef.handleFailedSave();
  1243. if (!newSong) this.$emit("savedError", song._id);
  1244. return new Toast(
  1245. "Invalid artists. You must have at least 1 artist and a maximum of 10 artists."
  1246. );
  1247. }
  1248. let error;
  1249. song.artists.forEach(artist => {
  1250. if (!validation.isLength(artist, 1, 64)) {
  1251. error = "Artist must have between 1 and 64 characters.";
  1252. return error;
  1253. }
  1254. if (artist === "NONE") {
  1255. error =
  1256. 'Invalid artist format. Artists are not allowed to be named "NONE".';
  1257. return error;
  1258. }
  1259. return false;
  1260. });
  1261. if (error) {
  1262. saveButtonRef.handleFailedSave();
  1263. if (!newSong) this.$emit("savedError", song._id);
  1264. return new Toast(error);
  1265. }
  1266. // Genres
  1267. error = undefined;
  1268. song.genres.forEach(genre => {
  1269. if (!validation.isLength(genre, 1, 32)) {
  1270. error = "Genre must have between 1 and 32 characters.";
  1271. return error;
  1272. }
  1273. if (!validation.regex.ascii.test(genre)) {
  1274. error =
  1275. "Invalid genre format. Only ascii characters are allowed.";
  1276. return error;
  1277. }
  1278. return false;
  1279. });
  1280. if (song.genres.length < 1 || song.genres.length > 16)
  1281. error = "You must have between 1 and 16 genres.";
  1282. if (error) {
  1283. saveButtonRef.handleFailedSave();
  1284. if (!newSong) this.$emit("savedError", song._id);
  1285. return new Toast(error);
  1286. }
  1287. error = undefined;
  1288. song.tags.forEach(tag => {
  1289. if (
  1290. !/^[a-zA-Z0-9_]{1,64}$|^[a-zA-Z0-9_]{1,64}\[[a-zA-Z0-9_]{1,64}\]$/.test(
  1291. tag
  1292. )
  1293. ) {
  1294. error = "Invalid tag format.";
  1295. return error;
  1296. }
  1297. return false;
  1298. });
  1299. if (error) {
  1300. saveButtonRef.handleFailedSave();
  1301. if (!newSong) this.$emit("savedError", song._id);
  1302. return new Toast(error);
  1303. }
  1304. // Thumbnail
  1305. if (!validation.isLength(song.thumbnail, 1, 256)) {
  1306. saveButtonRef.handleFailedSave();
  1307. if (!newSong) this.$emit("savedError", song._id);
  1308. return new Toast(
  1309. "Thumbnail must have between 8 and 256 characters."
  1310. );
  1311. }
  1312. if (this.useHTTPS && song.thumbnail.indexOf("https://") !== 0) {
  1313. saveButtonRef.handleFailedSave();
  1314. if (!newSong) this.$emit("savedError", song._id);
  1315. return new Toast('Thumbnail must start with "https://".');
  1316. }
  1317. if (
  1318. !this.useHTTPS &&
  1319. song.thumbnail.indexOf("http://") !== 0 &&
  1320. song.thumbnail.indexOf("https://") !== 0
  1321. ) {
  1322. saveButtonRef.handleFailedSave();
  1323. if (!newSong) this.$emit("savedError", song._id);
  1324. return new Toast('Thumbnail must start with "http://".');
  1325. }
  1326. saveButtonRef.status = "saving";
  1327. if (newSong)
  1328. return this.socket.dispatch(`songs.create`, song, res => {
  1329. new Toast(res.message);
  1330. if (res.status === "error") {
  1331. saveButtonRef.handleFailedSave();
  1332. return;
  1333. }
  1334. saveButtonRef.handleSuccessfulSave();
  1335. this.closeModal("editSong");
  1336. });
  1337. return this.socket.dispatch(`songs.update`, song._id, song, res => {
  1338. new Toast(res.message);
  1339. if (res.status === "error") {
  1340. saveButtonRef.handleFailedSave();
  1341. this.$emit("savedError", song._id);
  1342. return;
  1343. }
  1344. this.updateOriginalSong(song);
  1345. if (verify) {
  1346. saveButtonRef.status = "verifying";
  1347. this.verify(this.song._id, success => {
  1348. if (success) {
  1349. saveButtonRef.handleSuccessfulSave();
  1350. this.$emit("savedSuccess", song._id);
  1351. if (closeOrNext && this.bulk)
  1352. this.$emit("nextSong");
  1353. else if (closeOrNext) this.closeModal("editSong");
  1354. } else {
  1355. saveButtonRef.handleFailedSave();
  1356. this.$emit("savedError", song._id);
  1357. }
  1358. });
  1359. return;
  1360. }
  1361. saveButtonRef.handleSuccessfulSave();
  1362. this.$emit("savedSuccess", song._id);
  1363. if (!closeOrNext) return;
  1364. if (this.bulk) this.$emit("nextSong");
  1365. else this.closeModal("editSong");
  1366. });
  1367. },
  1368. editNextSong() {
  1369. this.$emit("nextSong");
  1370. },
  1371. toggleFlag() {
  1372. this.$emit("toggleFlag");
  1373. },
  1374. getAlbumData(type) {
  1375. if (!this.song.discogs) return;
  1376. if (type === "title")
  1377. this.updateSongField({
  1378. field: "title",
  1379. value: this.song.discogs.track.title
  1380. });
  1381. if (type === "albumArt")
  1382. this.updateSongField({
  1383. field: "thumbnail",
  1384. value: this.song.discogs.album.albumArt
  1385. });
  1386. if (type === "genres")
  1387. this.updateSongField({
  1388. field: "genres",
  1389. value: JSON.parse(
  1390. JSON.stringify(this.song.discogs.album.genres)
  1391. )
  1392. });
  1393. if (type === "artists")
  1394. this.updateSongField({
  1395. field: "artists",
  1396. value: JSON.parse(
  1397. JSON.stringify(this.song.discogs.album.artists)
  1398. )
  1399. });
  1400. },
  1401. fillDuration() {
  1402. this.song.duration =
  1403. this.youtubeVideoDuration - this.song.skipDuration;
  1404. },
  1405. settings(type) {
  1406. switch (type) {
  1407. case "stop":
  1408. this.stopVideo();
  1409. this.pauseVideo(true);
  1410. break;
  1411. case "pause":
  1412. this.pauseVideo(true);
  1413. break;
  1414. case "play":
  1415. this.pauseVideo(false);
  1416. break;
  1417. case "skipToLast10Secs":
  1418. this.skipToLast10SecsPressed = true;
  1419. this.seekTo(
  1420. this.song.duration - 10 + this.song.skipDuration
  1421. );
  1422. break;
  1423. default:
  1424. break;
  1425. }
  1426. },
  1427. play() {
  1428. if (
  1429. this.video.player.getVideoData().video_id !==
  1430. this.song.youtubeId
  1431. ) {
  1432. this.song.duration = -1;
  1433. this.loadVideoById(this.song.youtubeId, this.song.skipDuration);
  1434. }
  1435. this.settings("play");
  1436. },
  1437. seekTo(position) {
  1438. if (!this.video.paused) this.settings("play");
  1439. this.video.player.seekTo(position);
  1440. },
  1441. changeVolume() {
  1442. const volume = this.volumeSliderValue;
  1443. localStorage.setItem("volume", volume / 100);
  1444. this.video.player.setVolume(volume / 100);
  1445. if (volume > 0) {
  1446. this.video.player.unMute();
  1447. this.muted = false;
  1448. }
  1449. },
  1450. toggleMute() {
  1451. const previousVolume = parseFloat(localStorage.getItem("volume"));
  1452. const volume =
  1453. this.video.player.getVolume() * 100 <= 0 ? previousVolume : 0;
  1454. this.muted = !this.muted;
  1455. this.volumeSliderValue = volume * 100;
  1456. this.video.player.setVolume(volume);
  1457. if (!this.muted) localStorage.setItem("volume", volume);
  1458. },
  1459. increaseVolume() {
  1460. const previousVolume = parseInt(localStorage.getItem("volume"));
  1461. let volume = previousVolume + 5;
  1462. this.muted = false;
  1463. if (volume > 100) volume = 100;
  1464. this.volumeSliderValue = volume * 100;
  1465. this.video.player.setVolume(volume);
  1466. localStorage.setItem("volume", volume);
  1467. },
  1468. addTag(type, value) {
  1469. if (type === "genres") {
  1470. const genre = value || this.genreInputValue.trim();
  1471. if (
  1472. this.song.genres
  1473. .map(genre => genre.toLowerCase())
  1474. .indexOf(genre.toLowerCase()) !== -1
  1475. )
  1476. return new Toast("Genre already exists");
  1477. if (genre) {
  1478. this.song.genres.push(genre);
  1479. this.genreInputValue = "";
  1480. return false;
  1481. }
  1482. return new Toast("Genre cannot be empty");
  1483. }
  1484. if (type === "artists") {
  1485. const artist = value || this.artistInputValue;
  1486. if (this.song.artists.indexOf(artist) !== -1)
  1487. return new Toast("Artist already exists");
  1488. if (artist !== "") {
  1489. this.song.artists.push(artist);
  1490. this.artistInputValue = "";
  1491. return false;
  1492. }
  1493. return new Toast("Artist cannot be empty");
  1494. }
  1495. if (type === "tags") {
  1496. const tag = value || this.tagInputValue;
  1497. if (this.song.tags.indexOf(tag) !== -1)
  1498. return new Toast("Tag already exists");
  1499. if (tag !== "") {
  1500. this.song.tags.push(tag);
  1501. this.tagInputValue = "";
  1502. return false;
  1503. }
  1504. return new Toast("Tag cannot be empty");
  1505. }
  1506. return false;
  1507. },
  1508. removeTag(type, value) {
  1509. if (type === "genres")
  1510. this.song.genres.splice(this.song.genres.indexOf(value), 1);
  1511. else if (type === "artists")
  1512. this.song.artists.splice(this.song.artists.indexOf(value), 1);
  1513. else if (type === "tags")
  1514. this.song.tags.splice(this.song.tags.indexOf(value), 1);
  1515. },
  1516. drawCanvas() {
  1517. if (!this.songDataLoaded) return;
  1518. const canvasElement = this.$refs.durationCanvas;
  1519. const ctx = canvasElement.getContext("2d");
  1520. const videoDuration = Number(this.youtubeVideoDuration);
  1521. const skipDuration = Number(this.song.skipDuration);
  1522. const duration = Number(this.song.duration);
  1523. const afterDuration = videoDuration - (skipDuration + duration);
  1524. const width = 530;
  1525. const currentTime =
  1526. this.video.player && this.video.player.getCurrentTime
  1527. ? this.video.player.getCurrentTime()
  1528. : 0;
  1529. const widthSkipDuration = (skipDuration / videoDuration) * width;
  1530. const widthDuration = (duration / videoDuration) * width;
  1531. const widthAfterDuration = (afterDuration / videoDuration) * width;
  1532. const widthCurrentTime = (currentTime / videoDuration) * width;
  1533. const skipDurationColor = "#F42003";
  1534. const durationColor = "#03A9F4";
  1535. const afterDurationColor = "#41E841";
  1536. const currentDurationColor = "#3b25e8";
  1537. ctx.fillStyle = skipDurationColor;
  1538. ctx.fillRect(0, 0, widthSkipDuration, 20);
  1539. ctx.fillStyle = durationColor;
  1540. ctx.fillRect(widthSkipDuration, 0, widthDuration, 20);
  1541. ctx.fillStyle = afterDurationColor;
  1542. ctx.fillRect(
  1543. widthSkipDuration + widthDuration,
  1544. 0,
  1545. widthAfterDuration,
  1546. 20
  1547. );
  1548. ctx.fillStyle = currentDurationColor;
  1549. ctx.fillRect(widthCurrentTime, 0, 1, 20);
  1550. },
  1551. setTrackPosition(event) {
  1552. this.seekTo(
  1553. Number(
  1554. Number(this.video.player.getDuration()) *
  1555. ((event.pageX -
  1556. event.target.getBoundingClientRect().left) /
  1557. 530)
  1558. )
  1559. );
  1560. },
  1561. toggleGenreHelper() {
  1562. this.$refs.genreHelper.toggleBox();
  1563. },
  1564. resetGenreHelper() {
  1565. this.$refs.genreHelper.resetBox();
  1566. },
  1567. sendActivityWatchVideoData() {
  1568. if (!this.video.paused) {
  1569. if (this.activityWatchVideoLastStatus !== "playing") {
  1570. this.activityWatchVideoLastStatus = "playing";
  1571. if (
  1572. this.song.skipDuration > 0 &&
  1573. parseFloat(this.youtubeVideoCurrentTime) === 0
  1574. ) {
  1575. this.activityWatchVideoLastStartDuration = Math.floor(
  1576. this.song.skipDuration +
  1577. parseFloat(this.youtubeVideoCurrentTime)
  1578. );
  1579. } else {
  1580. this.activityWatchVideoLastStartDuration = Math.floor(
  1581. parseFloat(this.youtubeVideoCurrentTime)
  1582. );
  1583. }
  1584. }
  1585. const videoData = {
  1586. title: this.song.title,
  1587. artists: this.song.artists
  1588. ? this.song.artists.join(", ")
  1589. : null,
  1590. youtubeId: this.song.youtubeId,
  1591. muted: this.muted,
  1592. volume: this.volumeSliderValue / 100,
  1593. startedDuration:
  1594. this.activityWatchVideoLastStartDuration <= 0
  1595. ? 0
  1596. : this.activityWatchVideoLastStartDuration,
  1597. source: `editSong#${this.song.youtubeId}`,
  1598. hostname: window.location.hostname
  1599. };
  1600. aw.sendVideoData(videoData);
  1601. } else {
  1602. this.activityWatchVideoLastStatus = "not_playing";
  1603. }
  1604. },
  1605. verify(id, cb) {
  1606. if (this.newSong) this.song.verified = true;
  1607. else
  1608. this.socket.dispatch("songs.verify", id, res => {
  1609. new Toast(res.message);
  1610. if (cb) cb(res.status === "success");
  1611. });
  1612. },
  1613. unverify(id) {
  1614. if (this.newSong) this.song.verified = false;
  1615. else
  1616. this.socket.dispatch("songs.unverify", id, res => {
  1617. new Toast(res.message);
  1618. });
  1619. },
  1620. remove(id) {
  1621. this.socket.dispatch("songs.remove", id, res => {
  1622. new Toast(res.message);
  1623. });
  1624. },
  1625. confirmAction(confirm) {
  1626. this.confirm = confirm;
  1627. this.updateConfirmMessage(confirm.message);
  1628. this.openModal("editSongConfirm");
  1629. },
  1630. handleConfirmed() {
  1631. const { action, params } = this.confirm;
  1632. if (typeof this[action] === "function") {
  1633. if (params) this[action](params);
  1634. else this[action]();
  1635. }
  1636. this.confirm = {
  1637. message: "",
  1638. action: "",
  1639. params: null
  1640. };
  1641. },
  1642. onCloseModal() {
  1643. const songStringified = JSON.stringify({
  1644. ...this.song,
  1645. verified: null
  1646. });
  1647. const originalSongStringified = JSON.stringify({
  1648. ...this.originalSong,
  1649. verified: null
  1650. });
  1651. const unsavedChanges = songStringified !== originalSongStringified;
  1652. if (unsavedChanges) {
  1653. return this.confirmAction({
  1654. message:
  1655. "You have unsaved changes. Are you sure you want to discard unsaved changes?",
  1656. action: "closeThisModal",
  1657. params: null
  1658. });
  1659. }
  1660. return this.closeThisModal();
  1661. },
  1662. closeThisModal() {
  1663. if (this.bulk) this.$emit("close");
  1664. else this.closeModal("editSong");
  1665. },
  1666. ...mapActions("modals/importAlbum", ["selectDiscogsAlbum"]),
  1667. ...mapActions({
  1668. showTab(dispatch, payload) {
  1669. this.$refs[`${payload}-tab`].scrollIntoView({
  1670. block: "nearest"
  1671. });
  1672. return dispatch("modals/editSong/showTab", payload);
  1673. }
  1674. }),
  1675. ...mapActions("modals/editSong", [
  1676. "stopVideo",
  1677. "loadVideoById",
  1678. "pauseVideo",
  1679. "getCurrentTime",
  1680. "setSong",
  1681. "resetSong",
  1682. "updateOriginalSong",
  1683. "updateSongField",
  1684. "updateReports"
  1685. ]),
  1686. ...mapActions("modals/confirm", ["updateConfirmMessage"]),
  1687. ...mapActions("modalVisibility", ["closeModal", "openModal"])
  1688. }
  1689. };
  1690. </script>
  1691. <style lang="less" scoped>
  1692. .night-mode {
  1693. .edit-section,
  1694. .player-section,
  1695. #tabs-container {
  1696. background-color: var(--dark-grey-3) !important;
  1697. border: 0 !important;
  1698. .tab {
  1699. border: 0 !important;
  1700. }
  1701. }
  1702. #tabs-container #tab-selection .button {
  1703. background: var(--dark-grey) !important;
  1704. color: var(--white) !important;
  1705. }
  1706. .left-section {
  1707. .edit-section {
  1708. .album-get-button,
  1709. .duration-fill-button,
  1710. .add-button {
  1711. &:focus,
  1712. &:hover {
  1713. border: none !important;
  1714. }
  1715. }
  1716. }
  1717. }
  1718. #durationCanvas {
  1719. background-color: var(--dark-grey-2) !important;
  1720. }
  1721. }
  1722. .modal-card-body {
  1723. display: flex;
  1724. }
  1725. .notice-container {
  1726. display: flex;
  1727. flex: 1;
  1728. justify-content: center;
  1729. h4 {
  1730. margin: auto;
  1731. }
  1732. }
  1733. .left-section {
  1734. flex-basis: unset !important;
  1735. height: 100%;
  1736. display: flex;
  1737. flex-direction: column;
  1738. margin-right: 16px;
  1739. .top-section {
  1740. display: flex;
  1741. .player-section {
  1742. width: 530px;
  1743. display: flex;
  1744. flex-direction: column;
  1745. border: 1px solid var(--light-grey-3);
  1746. border-radius: @border-radius;
  1747. overflow: hidden;
  1748. #durationCanvas {
  1749. background-color: var(--light-grey-2);
  1750. }
  1751. .player-error {
  1752. display: flex;
  1753. height: 318px;
  1754. width: 530px;
  1755. align-items: center;
  1756. * {
  1757. margin: 0;
  1758. flex: 1;
  1759. font-size: 30px;
  1760. text-align: center;
  1761. }
  1762. }
  1763. .player-footer {
  1764. display: flex;
  1765. justify-content: space-between;
  1766. height: 54px;
  1767. padding-left: 10px;
  1768. padding-right: 10px;
  1769. > * {
  1770. width: 33.3%;
  1771. display: flex;
  1772. align-items: center;
  1773. }
  1774. .player-footer-left {
  1775. flex: 1;
  1776. .button {
  1777. width: 75px;
  1778. &:not(:first-of-type) {
  1779. margin-left: 5px;
  1780. }
  1781. }
  1782. }
  1783. .player-footer-center {
  1784. justify-content: center;
  1785. align-items: center;
  1786. flex: 2;
  1787. font-size: 18px;
  1788. font-weight: 400;
  1789. width: 200px;
  1790. margin: 0 5px;
  1791. img {
  1792. height: 21px;
  1793. margin-right: 12px;
  1794. filter: invert(26%) sepia(54%) saturate(6317%)
  1795. hue-rotate(2deg) brightness(92%) contrast(115%);
  1796. }
  1797. }
  1798. .player-footer-right {
  1799. justify-content: right;
  1800. flex: 1;
  1801. #volume-control {
  1802. margin: 3px;
  1803. margin-top: 0;
  1804. display: flex;
  1805. align-items: center;
  1806. cursor: pointer;
  1807. .volume-slider {
  1808. width: 100%;
  1809. padding: 0 15px;
  1810. background: transparent;
  1811. min-width: 100px;
  1812. }
  1813. input[type="range"] {
  1814. -webkit-appearance: none;
  1815. margin: 7.3px 0;
  1816. }
  1817. input[type="range"]:focus {
  1818. outline: none;
  1819. }
  1820. input[type="range"]::-webkit-slider-runnable-track {
  1821. width: 100%;
  1822. height: 5.2px;
  1823. cursor: pointer;
  1824. box-shadow: 0;
  1825. background: var(--light-grey-3);
  1826. border-radius: 0;
  1827. border: 0;
  1828. }
  1829. input[type="range"]::-webkit-slider-thumb {
  1830. box-shadow: 0;
  1831. border: 0;
  1832. height: 19px;
  1833. width: 19px;
  1834. border-radius: 15px;
  1835. background: var(--primary-color);
  1836. cursor: pointer;
  1837. -webkit-appearance: none;
  1838. margin-top: -6.5px;
  1839. }
  1840. input[type="range"]::-moz-range-track {
  1841. width: 100%;
  1842. height: 5.2px;
  1843. cursor: pointer;
  1844. box-shadow: 0;
  1845. background: var(--light-grey-3);
  1846. border-radius: 0;
  1847. border: 0;
  1848. }
  1849. input[type="range"]::-moz-range-thumb {
  1850. box-shadow: 0;
  1851. border: 0;
  1852. height: 19px;
  1853. width: 19px;
  1854. border-radius: 15px;
  1855. background: var(--primary-color);
  1856. cursor: pointer;
  1857. -webkit-appearance: none;
  1858. margin-top: -6.5px;
  1859. }
  1860. input[type="range"]::-ms-track {
  1861. width: 100%;
  1862. height: 5.2px;
  1863. cursor: pointer;
  1864. box-shadow: 0;
  1865. background: var(--light-grey-3);
  1866. border-radius: 1.3px;
  1867. }
  1868. input[type="range"]::-ms-fill-lower {
  1869. background: var(--light-grey-3);
  1870. border: 0;
  1871. border-radius: 0;
  1872. box-shadow: 0;
  1873. }
  1874. input[type="range"]::-ms-fill-upper {
  1875. background: var(--light-grey-3);
  1876. border: 0;
  1877. border-radius: 0;
  1878. box-shadow: 0;
  1879. }
  1880. input[type="range"]::-ms-thumb {
  1881. box-shadow: 0;
  1882. border: 0;
  1883. height: 15px;
  1884. width: 15px;
  1885. border-radius: 15px;
  1886. background: var(--primary-color);
  1887. cursor: pointer;
  1888. -webkit-appearance: none;
  1889. margin-top: 1.5px;
  1890. }
  1891. }
  1892. }
  1893. }
  1894. }
  1895. .thumbnail-preview {
  1896. width: 189px;
  1897. height: 189px;
  1898. margin-left: 16px;
  1899. }
  1900. }
  1901. .edit-section {
  1902. width: 735px;
  1903. border: 1px solid var(--light-grey-3);
  1904. flex: 1;
  1905. margin-top: 16px;
  1906. border-radius: @border-radius;
  1907. .album-get-button {
  1908. background-color: var(--purple);
  1909. color: var(--white);
  1910. width: 32px;
  1911. text-align: center;
  1912. border-width: 0;
  1913. }
  1914. .duration-fill-button {
  1915. background-color: var(--dark-red);
  1916. color: var(--white);
  1917. width: 32px;
  1918. text-align: center;
  1919. border-width: 0;
  1920. }
  1921. .add-button {
  1922. background-color: var(--primary-color) !important;
  1923. width: 32px;
  1924. i {
  1925. font-size: 32px;
  1926. }
  1927. }
  1928. .album-get-button,
  1929. .duration-fill-button,
  1930. .add-button {
  1931. &:focus,
  1932. &:hover {
  1933. filter: contrast(0.75);
  1934. border: 1px solid var(--black) !important;
  1935. }
  1936. }
  1937. > div {
  1938. margin: 16px !important;
  1939. }
  1940. input {
  1941. width: 100%;
  1942. }
  1943. .title-container {
  1944. width: calc((100% - 32px) / 2);
  1945. }
  1946. .duration-container {
  1947. margin-right: 16px;
  1948. margin-left: 16px;
  1949. width: calc((100% - 32px) / 4);
  1950. }
  1951. .skip-duration-container {
  1952. width: calc((100% - 32px) / 4);
  1953. }
  1954. .album-art-container {
  1955. margin-right: 16px;
  1956. width: calc((100% - 16px) / 3 * 2);
  1957. }
  1958. .youtube-id-container {
  1959. width: calc((100% - 16px) / 3);
  1960. }
  1961. .artists-container {
  1962. width: calc((100% - 32px) / 3);
  1963. position: relative;
  1964. }
  1965. .genres-container {
  1966. width: calc((100% - 32px) / 3);
  1967. margin-left: 16px;
  1968. margin-right: 16px;
  1969. position: relative;
  1970. label {
  1971. display: flex;
  1972. i {
  1973. font-size: 15px;
  1974. align-self: center;
  1975. margin-left: 5px;
  1976. color: var(--primary-color);
  1977. cursor: pointer;
  1978. -webkit-user-select: none;
  1979. -moz-user-select: none;
  1980. -ms-user-select: none;
  1981. user-select: none;
  1982. }
  1983. }
  1984. }
  1985. .tags-container {
  1986. width: calc((100% - 32px) / 3);
  1987. position: relative;
  1988. }
  1989. .list-item-circle {
  1990. background-color: var(--primary-color);
  1991. width: 16px;
  1992. height: 16px;
  1993. border-radius: 8px;
  1994. cursor: pointer;
  1995. margin-right: 8px;
  1996. float: left;
  1997. -webkit-touch-callout: none;
  1998. -webkit-user-select: none;
  1999. -khtml-user-select: none;
  2000. -moz-user-select: none;
  2001. -ms-user-select: none;
  2002. user-select: none;
  2003. i {
  2004. color: var(--primary-color);
  2005. font-size: 14px;
  2006. margin-left: 1px;
  2007. position: relative;
  2008. top: -1px;
  2009. }
  2010. }
  2011. .list-item-circle:hover,
  2012. .list-item-circle:focus {
  2013. i {
  2014. color: var(--white);
  2015. }
  2016. }
  2017. .list-item > p {
  2018. line-height: 16px;
  2019. word-wrap: break-word;
  2020. width: calc(100% - 24px);
  2021. left: 24px;
  2022. float: left;
  2023. margin-bottom: 8px;
  2024. }
  2025. .list-item:last-child > p {
  2026. margin-bottom: 0;
  2027. }
  2028. }
  2029. }
  2030. .right-section {
  2031. flex-basis: unset !important;
  2032. flex-grow: 0 !important;
  2033. display: flex;
  2034. height: 100%;
  2035. #tabs-container {
  2036. width: 376px;
  2037. #tab-selection {
  2038. display: flex;
  2039. overflow-x: auto;
  2040. .button {
  2041. border-radius: @border-radius @border-radius 0 0;
  2042. border: 0;
  2043. text-transform: uppercase;
  2044. font-size: 14px;
  2045. color: var(--dark-grey-3);
  2046. background-color: var(--light-grey-2);
  2047. flex-grow: 1;
  2048. height: 32px;
  2049. &:not(:first-of-type) {
  2050. margin-left: 5px;
  2051. }
  2052. }
  2053. .selected {
  2054. background-color: var(--primary-color) !important;
  2055. color: var(--white) !important;
  2056. font-weight: 600;
  2057. }
  2058. }
  2059. .tab {
  2060. border: 1px solid var(--light-grey-3);
  2061. border-radius: 0 0 @border-radius @border-radius;
  2062. padding: 15px;
  2063. height: calc(100% - 32px);
  2064. overflow: auto;
  2065. }
  2066. }
  2067. }
  2068. .modal-card-foot .is-primary {
  2069. width: 200px;
  2070. }
  2071. :deep(.autosuggest-container) {
  2072. top: unset;
  2073. }
  2074. </style>