Playlists.vue 8.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345
  1. <template>
  2. <div>
  3. <page-metadata title="Admin | Playlists" />
  4. <div class="container">
  5. <div class="button-row">
  6. <confirm
  7. placement="bottom"
  8. @confirm="deleteOrphanedStationPlaylists()"
  9. >
  10. <button class="button is-danger">
  11. Delete orphaned station playlists
  12. </button>
  13. </confirm>
  14. <confirm
  15. placement="bottom"
  16. @confirm="deleteOrphanedGenrePlaylists()"
  17. >
  18. <button class="button is-danger">
  19. Delete orphaned genre playlists
  20. </button>
  21. </confirm>
  22. <confirm
  23. placement="bottom"
  24. @confirm="requestOrphanedPlaylistSongs()"
  25. >
  26. <button class="button is-danger">
  27. Request orphaned playlist songs
  28. </button>
  29. </confirm>
  30. <confirm
  31. placement="bottom"
  32. @confirm="clearAndRefillAllStationPlaylists()"
  33. >
  34. <button class="button is-danger">
  35. Clear and refill all station playlists
  36. </button>
  37. </confirm>
  38. <confirm
  39. placement="bottom"
  40. @confirm="clearAndRefillAllGenrePlaylists()"
  41. >
  42. <button class="button is-danger">
  43. Clear and refill all genre playlists
  44. </button>
  45. </confirm>
  46. <confirm
  47. placement="bottom"
  48. @confirm="createMissingGenrePlaylists()"
  49. >
  50. <button class="button is-danger">
  51. Create missing genre playlists
  52. </button>
  53. </confirm>
  54. </div>
  55. <table class="table">
  56. <thead>
  57. <tr>
  58. <td>Display name</td>
  59. <td>Type</td>
  60. <td>Is user modifiable</td>
  61. <td>Privacy</td>
  62. <td>Songs #</td>
  63. <td>Playlist length</td>
  64. <td>Created by</td>
  65. <td>Created at</td>
  66. <td>Created for</td>
  67. <td>Playlist id</td>
  68. <td>Options</td>
  69. </tr>
  70. </thead>
  71. <tbody>
  72. <tr v-for="playlist in playlists" :key="playlist._id">
  73. <td>{{ playlist.displayName }}</td>
  74. <td>{{ playlist.type }}</td>
  75. <td>{{ playlist.isUserModifiable }}</td>
  76. <td>{{ playlist.privacy }}</td>
  77. <td>{{ playlist.songs.length }}</td>
  78. <td>{{ totalLengthForPlaylist(playlist.songs) }}</td>
  79. <td v-if="playlist.createdBy === 'Musare'">Musare</td>
  80. <td v-else>
  81. <user-id-to-username
  82. :user-id="playlist.createdBy"
  83. :link="true"
  84. />
  85. </td>
  86. <td :title="new Date(playlist.createdAt)">
  87. {{ getDateFormatted(playlist.createdAt) }}
  88. </td>
  89. <td>{{ playlist.createdFor }}</td>
  90. <td>{{ playlist._id }}</td>
  91. <td>
  92. <button
  93. class="button is-primary"
  94. @click="edit(playlist._id)"
  95. >
  96. View
  97. </button>
  98. </td>
  99. </tr>
  100. </tbody>
  101. </table>
  102. </div>
  103. <edit-playlist v-if="modals.editPlaylist" sector="admin" />
  104. <edit-song v-if="modals.editSong" song-type="songs" />
  105. <report v-if="modals.report" />
  106. </div>
  107. </template>
  108. <script>
  109. import { mapState, mapActions, mapGetters } from "vuex";
  110. import { defineAsyncComponent } from "vue";
  111. import Toast from "toasters";
  112. import Confirm from "@/components/Confirm.vue";
  113. import UserIdToUsername from "@/components/UserIdToUsername.vue";
  114. import ws from "@/ws";
  115. import utils from "../../../../js/utils";
  116. export default {
  117. components: {
  118. EditPlaylist: defineAsyncComponent(() =>
  119. import("@/components/modals/EditPlaylist")
  120. ),
  121. UserIdToUsername,
  122. Report: defineAsyncComponent(() =>
  123. import("@/components/modals/Report.vue")
  124. ),
  125. EditSong: defineAsyncComponent(() =>
  126. import("@/components/modals/EditSong")
  127. ),
  128. Confirm
  129. },
  130. data() {
  131. return {
  132. utils
  133. };
  134. },
  135. computed: {
  136. ...mapState("modalVisibility", {
  137. modals: state => state.modals
  138. }),
  139. ...mapState("admin/playlists", {
  140. playlists: state => state.playlists
  141. }),
  142. ...mapGetters({
  143. socket: "websockets/getSocket"
  144. })
  145. },
  146. mounted() {
  147. this.socket.on("event:admin.playlist.created", res =>
  148. this.addPlaylist(res.data.playlist)
  149. );
  150. this.socket.on("event:admin.playlist.deleted", res =>
  151. this.removePlaylist(res.data.playlistId)
  152. );
  153. this.socket.on("event:admin.playlist.song.added", res =>
  154. this.addPlaylistSong({
  155. playlistId: res.data.playlistId,
  156. song: res.data.song
  157. })
  158. );
  159. this.socket.on("event:admin.playlist.song.removed", res =>
  160. this.removePlaylistSong({
  161. playlistId: res.data.playlistId,
  162. youtubeId: res.data.youtubeId
  163. })
  164. );
  165. this.socket.on("event:admin.playlist.displayName.updated", res =>
  166. this.updatePlaylistDisplayName({
  167. playlistId: res.data.playlistId,
  168. displayName: res.data.displayName
  169. })
  170. );
  171. this.socket.on("event:admin.playlist.privacy.updated", res =>
  172. this.updatePlaylistPrivacy({
  173. playlistId: res.data.playlistId,
  174. privacy: res.data.privacy
  175. })
  176. );
  177. ws.onConnect(this.init);
  178. },
  179. methods: {
  180. edit(playlistId) {
  181. this.editPlaylist(playlistId);
  182. this.openModal("editPlaylist");
  183. },
  184. init() {
  185. this.socket.dispatch("playlists.index", res => {
  186. if (res.status === "success") {
  187. this.setPlaylists(res.data.playlists);
  188. if (this.$route.query.playlistId) {
  189. const playlist = this.playlists.find(
  190. playlist =>
  191. playlist._id === this.$route.query.playlistId
  192. );
  193. if (playlist) this.edit(playlist._id);
  194. }
  195. }
  196. });
  197. this.socket.dispatch("apis.joinAdminRoom", "playlists", () => {});
  198. },
  199. getDateFormatted(createdAt) {
  200. const date = new Date(createdAt);
  201. const year = date.getFullYear();
  202. const month = `${date.getMonth() + 1}`.padStart(2, 0);
  203. const day = `${date.getDate()}`.padStart(2, 0);
  204. const hour = `${date.getHours()}`.padStart(2, 0);
  205. const minute = `${date.getMinutes()}`.padStart(2, 0);
  206. return `${year}-${month}-${day} ${hour}:${minute}`;
  207. },
  208. totalLengthForPlaylist(songs) {
  209. let length = 0;
  210. songs.forEach(song => {
  211. length += song.duration;
  212. });
  213. return this.utils.formatTimeLong(length);
  214. },
  215. deleteOrphanedStationPlaylists() {
  216. this.socket.dispatch(
  217. "playlists.deleteOrphanedStationPlaylists",
  218. res => {
  219. if (res.status === "success") new Toast(res.message);
  220. else new Toast(`Error: ${res.message}`);
  221. }
  222. );
  223. },
  224. deleteOrphanedGenrePlaylists() {
  225. this.socket.dispatch(
  226. "playlists.deleteOrphanedGenrePlaylists",
  227. res => {
  228. if (res.status === "success") new Toast(res.message);
  229. else new Toast(`Error: ${res.message}`);
  230. }
  231. );
  232. },
  233. requestOrphanedPlaylistSongs() {
  234. this.socket.dispatch(
  235. "playlists.requestOrphanedPlaylistSongs",
  236. res => {
  237. if (res.status === "success") new Toast(res.message);
  238. else new Toast(`Error: ${res.message}`);
  239. }
  240. );
  241. },
  242. clearAndRefillAllStationPlaylists() {
  243. this.socket.dispatch(
  244. "playlists.clearAndRefillAllStationPlaylists",
  245. res => {
  246. if (res.status === "success")
  247. new Toast({ content: res.message, timeout: 4000 });
  248. else
  249. new Toast({
  250. content: `Error: ${res.message}`,
  251. timeout: 4000
  252. });
  253. }
  254. );
  255. },
  256. clearAndRefillAllGenrePlaylists() {
  257. this.socket.dispatch(
  258. "playlists.clearAndRefillAllGenrePlaylists",
  259. res => {
  260. if (res.status === "success")
  261. new Toast({ content: res.message, timeout: 4000 });
  262. else
  263. new Toast({
  264. content: `Error: ${res.message}`,
  265. timeout: 4000
  266. });
  267. }
  268. );
  269. },
  270. createMissingGenrePlaylists() {
  271. this.socket.dispatch(
  272. "playlists.createMissingGenrePlaylists",
  273. data => {
  274. console.log(data.message);
  275. if (data.status !== "success")
  276. new Toast({
  277. content: `Error: ${data.message}`,
  278. timeout: 8000
  279. });
  280. else new Toast({ content: data.message, timeout: 4000 });
  281. }
  282. );
  283. },
  284. ...mapActions("modalVisibility", ["openModal"]),
  285. ...mapActions("user/playlists", ["editPlaylist"]),
  286. ...mapActions("admin/playlists", [
  287. "addPlaylist",
  288. "setPlaylists",
  289. "removePlaylist",
  290. "addPlaylistSong",
  291. "removePlaylistSong",
  292. "updatePlaylistDisplayName",
  293. "updatePlaylistPrivacy"
  294. ])
  295. }
  296. };
  297. </script>
  298. <style lang="scss" scoped>
  299. .night-mode {
  300. .table {
  301. color: var(--light-grey-2);
  302. background-color: var(--dark-grey-3);
  303. thead tr {
  304. background: var(--dark-grey-3);
  305. td {
  306. color: var(--white);
  307. }
  308. }
  309. tbody tr:hover {
  310. background-color: var(--dark-grey-4) !important;
  311. }
  312. tbody tr:nth-child(even) {
  313. background-color: var(--dark-grey-2);
  314. }
  315. strong {
  316. color: var(--light-grey-2);
  317. }
  318. }
  319. }
  320. td {
  321. vertical-align: middle;
  322. }
  323. .is-primary:focus {
  324. background-color: var(--primary-color) !important;
  325. }
  326. </style>