├── portable ├── locales ├── ko.json ├── nb_NO.json ├── ru.json ├── zh-Hans.json ├── nl.json └── ja.json ├── kmfrontend ├── src │ ├── locales │ │ ├── ko.json │ │ └── nl.json │ ├── react-app-env.d.ts │ ├── frontend │ │ ├── types │ │ │ ├── scope.d.ts │ │ │ ├── news.d.ts │ │ │ ├── tag.d.ts │ │ │ ├── playlist.d.ts │ │ │ ├── image.d.ts │ │ │ ├── kara.d.ts │ │ │ └── remote.d.ts │ │ ├── components │ │ │ ├── modals │ │ │ │ ├── PlayCurrentModal.scss │ │ │ │ ├── PollModal.scss │ │ │ │ ├── CropAvatarModal.scss │ │ │ │ ├── OnlineProfileModal.scss │ │ │ │ ├── AutoMixModal.scss │ │ │ │ ├── KaraMenuModal.scss │ │ │ │ ├── AdminMessageModal.scss │ │ │ │ ├── PlaylistModal.scss │ │ │ │ ├── ShutdownModal.tsx │ │ │ │ └── UsersModal.scss │ │ │ ├── decorators │ │ │ │ ├── KmAppHeaderDecorator.tsx │ │ │ │ ├── KmAppBodyDecorator.tsx │ │ │ │ ├── PlaylistMainDecorator.tsx │ │ │ │ └── KmAppWrapperDecorator.tsx │ │ │ ├── PlaylistPage.scss │ │ │ ├── karas │ │ │ │ ├── QuizScore.scss │ │ │ │ ├── ActionsButtons.scss │ │ │ │ ├── InlineTag.scss │ │ │ │ ├── CriteriasList.scss │ │ │ │ └── KaraList.scss │ │ │ ├── migrations │ │ │ │ └── Migration.tsx │ │ │ ├── generic │ │ │ │ ├── buttons │ │ │ │ │ ├── ShowVideoButton.tsx │ │ │ │ │ ├── AddKaraButton.tsx │ │ │ │ │ ├── UpvoteKaraButton.tsx │ │ │ │ │ └── MakeFavButton.tsx │ │ │ │ ├── RadioButton.scss │ │ │ │ ├── RadioButton.tsx │ │ │ │ ├── Switch.tsx │ │ │ │ └── Switch.scss │ │ │ ├── About.scss │ │ │ ├── NotfoundPage.tsx │ │ │ ├── NotfoundPage.scss │ │ │ ├── public │ │ │ │ ├── TagsList.scss │ │ │ │ ├── QuizPage.scss │ │ │ │ └── LyricsBox.scss │ │ │ └── WelcomePageArticle.tsx │ │ ├── styles │ │ │ ├── fonts │ │ │ │ └── Lato │ │ │ │ │ ├── Lato-Bold.eot │ │ │ │ │ ├── Lato-Bold.ttf │ │ │ │ │ ├── Lato-Bold.woff │ │ │ │ │ ├── Lato-Light.eot │ │ │ │ │ ├── Lato-Light.ttf │ │ │ │ │ ├── Lato-Bold.woff2 │ │ │ │ │ ├── Lato-Light.woff │ │ │ │ │ ├── Lato-Light.woff2 │ │ │ │ │ ├── Lato-Regular.eot │ │ │ │ │ ├── Lato-Regular.ttf │ │ │ │ │ ├── Lato-Regular.woff │ │ │ │ │ ├── Lato-Regular.woff2 │ │ │ │ │ └── stylesheet.scss │ │ │ ├── al-icons │ │ │ │ └── font │ │ │ │ │ ├── al-icons.eot │ │ │ │ │ ├── al-icons.ttf │ │ │ │ │ ├── al-icons.woff │ │ │ │ │ └── al-icons.woff2 │ │ │ ├── components │ │ │ │ ├── overlays.scss │ │ │ │ ├── titles.scss │ │ │ │ ├── blurred-bg.scss │ │ │ │ ├── details.scss │ │ │ │ ├── loader.scss │ │ │ │ ├── dropdowns.scss │ │ │ │ └── tags.scss │ │ │ └── start │ │ │ │ └── MigratePage.scss │ │ ├── global.d.ts │ │ └── KMFrontend.scss │ ├── assets │ │ ├── Klogo.png │ │ ├── blank.png │ │ ├── dame.jpg │ │ ├── noise.png │ │ ├── nanami.png │ │ ├── nanami.webp │ │ ├── nanami-cry.png │ │ ├── nanami-umu.png │ │ ├── nanami-cry.webp │ │ ├── nanami-hehe2.png │ │ ├── nanami-sing.png │ │ ├── nanami-sing.webp │ │ ├── nanami-smile.png │ │ ├── nanami-smile.webp │ │ ├── nanami-think.png │ │ ├── nanami-think.webp │ │ ├── nanami-umu.webp │ │ ├── tuto_karaline.png │ │ ├── nanami-shocked.png │ │ ├── nanami-shocked.webp │ │ ├── nanami-surpris.png │ │ ├── Logo-fond-transp.png │ │ ├── nanami-searching.gif │ │ └── Logo-final-fond-transparent.png │ ├── systempanel │ │ ├── components │ │ │ ├── karas │ │ │ │ └── CheckBoxTag.scss │ │ │ ├── Title.tsx │ │ │ ├── Loading.tsx │ │ │ └── OpenLyricsFileButton.tsx │ │ └── pages │ │ │ ├── Karas │ │ │ ├── KaraForm.scss │ │ │ ├── KaraListPage.tsx │ │ │ └── KaraNew.tsx │ │ │ ├── Options.tsx │ │ │ └── Tags │ │ │ └── TagsNew.tsx │ ├── store │ │ ├── GlobalStateProvider.tsx │ │ ├── reducers │ │ │ ├── modal.ts │ │ │ ├── settings.ts │ │ │ ├── auth.ts │ │ │ └── frontendContext.ts │ │ ├── types │ │ │ ├── modal.ts │ │ │ ├── auth.ts │ │ │ └── settings.ts │ │ ├── actions │ │ │ └── modal.ts │ │ ├── context.ts │ │ └── useGlobalState.ts │ ├── utils │ │ ├── components │ │ │ ├── Loading.scss │ │ │ ├── Loading.tsx │ │ │ └── ProfilePicture.tsx │ │ ├── i18next-scanner.config.js │ │ ├── PrivateRoute.tsx │ │ ├── polyfills.ts │ │ └── i18n.ts │ ├── toast.scss │ └── index.tsx ├── .yarnrc ├── public │ ├── favicon.ico │ └── manifest.json ├── .prettierrc.json ├── vite.config.mts ├── tsconfig.json ├── index.html └── eslint.config.mjs ├── migrations ├── 20190821192135.do.removeStatsView.sql ├── 20250618000000.do.betterSSHKeys.sql ├── 20200122135323.do.removeRepoTable.sql ├── 20201202000000.do.removePLMediasTable.sql ├── 20230822000000.do.removeGain.sql ├── 20250731000000.do.branchRepositoryIsNowMandatory.sql ├── 20200606000000.do.dropURLDownloads.sql ├── 20210222000000.do.dropUserFlagOnline.sql ├── 20220508000000.do.removeYearsView.sql ├── 20200803000000.do.addEndedAtToSessions.sql ├── 20210510000000.do.addLocation.sql ├── 20220507000000.do.OneMoreGuest.sql ├── 20221118000000.do.removeLangFromGuestAccount.sql ├── 20230831000000.do.alsoRemoveGainInAllKaras.sql ├── 20190214102323.do.addDefaultNullKaraOrder.sql ├── 20210221000000.do.revertFlagAutoSortByLike.sql ├── 20210518000000.do.addSendStatsToUsers.sql ├── 20210524000000.do.updateGuestsSendStats.sql ├── 20210821000001.do.addLanguageToProfile.sql ├── 20190515144041.do.removeSubfileNotNullConstraint.sql ├── 20190730142823.do.wipeBlacklistCriterias.sql ├── 20210302000000.do.addKMOnlineMigration.sql ├── 20210821000000.do.dropSeriesLangMode.sql ├── 20220228000000.do.addDescriptionToTags.sql ├── 20220730000000.do.addExternalDatabaseIds.sql ├── 20220613000000.do.addTemporaryFlagToUsers.sql ├── 20241103000000.do.addSongname.sql ├── 20250515000000.do.recreateSongname.sql ├── 20200203133537.do.addKIDtoDownloads.sql ├── 20210509231128.do.migrateBLC.sql ├── 20220331230559.do.addCollectionsMigration.sql ├── 20240325000000.do.moveInstanceIDAndTokenFromDBToConfig.sql ├── 20241215000000.do.FixMigrateLyricInfos.sql ├── 20250330000000.do.addPlayedAtPlaylist.sql ├── 20210430000000.do.addKIDtoDownloads.sql ├── 20210508000000.do.addBulldozerVideosMigration.sql ├── 20220516000000.do.deprecateCriterias1004and1005.sql ├── 20190620101811.do.addDownloadBLCUniqueValue.sql ├── 20210216000000.do.addFlagAutoSortByLikeToPlaylistTable.sql ├── 20220504000000.do.removeMoreGuests.sql ├── 20191212100536.do.addPrivateFlagForSessions.sql ├── 20221114000000.do.addSongtypeSongorderToSearchVector.sql ├── 20191215162839.do.makeNicknameMandatory.sql ├── 20210719000000.do.removeSubchecksum.sql ├── 20201206000000.do.addTutorialDoneFlagToUsers.sql ├── 20190525113043.do.addFlagVisibleColumnToPLC.sql ├── 20211009000000.do.fixCriteriasInPLCTable.sql ├── 20211010000001.do.addFlagParentsOnlyToUsers.sql ├── 20200627000000.do.assFullIndexPlayed.sql ├── 20210904000000.do.removeCommentAddCriteria.sql ├── 20220607000000.do.aBarrelRoll.sql ├── 20231029000000.do.addFromDisplayType.sql ├── 20200123153420.do.addRepoToDownload.sql ├── 20210127000000.do.addMigrationFrontendTable.sql ├── 20210118000000.do.addOnlineRequestedTable.sql ├── 20221001000000.do.addAnimeList.sql ├── 20251001000000.do.changeKaraRelationConstraints.sql ├── 20210227000000.do.addFlagAcceptRefuse.sql ├── 20200708000000.do.addMediasTable.sql ├── 20241203000000.do.migrateLyricInfos.sql ├── 20250224000000.do.addSongsPlayedAndLeftToPlaylistInfo.sql ├── 20190410081710.do.addBLCDownloadTable.sql ├── 20190215154455.do.addKaraSerieLangMaterializedView.sql ├── 20231128000000.do.addAnnouncePositionToKara.sql ├── 20250728000000.do.updatePlaylistServerFields.sql ├── 20190617090553.do.removeOldGuestNames.sql ├── 20210501000000.do.addDownloadedToKaras.sql ├── 20240527000001.do.fixSingergroupsSortablePart2.sql ├── 20250530000000.do.decouplingOnlineConfig.sql ├── 20190226223300.do.createDownloadTable.sql ├── 20200622000000.do.changeFlagPlaying.sql ├── 20190524114916.do.addLangModeColumnsUser.sql ├── 20210212010000.do.addUniqueIndexToAllTagsAndYearsView.sql ├── 20210928000000.do.addAndOrSmatPlaylists.sql ├── 20201123141624.do.guestsRename.sql ├── 20220328000000.do.removeDuplicateCriterias.sql ├── 20190410125416.do.karaTagRestrictToCascade.sql ├── 20210427000000.do.gitMigrationDownload.sql ├── 20230808000000.do.renameidplcontentToPLCID.sql ├── 20220117000000.do.KaraParentsConstraints.sql ├── 20211217182244.do.newUserFields.sql ├── 20240123000000.do.migratingGuestNames.sql ├── 20200516000000.do.addBLCSets.sql ├── 20211205000000.do.removeModifiedAtTags.sql ├── 20211016000000.do.addPlaylistSmartLimits.sql ├── 20210712000000.do.renameSephirothGuest.sql ├── 20230615000000.do.addFallbackPlaylistType.sql ├── 20220425000000.do.reworkTagViewCollections.sql ├── 20191026124159.do.AddTagKaracountWithType.sql ├── 20200329131617.do.addUserConstraints.sql ├── 20210422233728.do.goToPlaids.sql ├── 20240527000002.do.fixSingergroupsSortablePart3.sql ├── 20190522101040.do.addSessionTableAndColumns.sql ├── 20200808000000.do.addNoLiveDownloadFlagToTags.sql └── 20250727000000.do.changeSearchableFieldsInAllKaras.sql ├── src ├── types │ ├── frontend.d.ts │ ├── feeds.d.ts │ ├── database │ │ ├── upvote.d.ts │ │ ├── user.d.ts │ │ ├── migrationsFrontend.d.ts │ │ ├── download.d.ts │ │ ├── stats.d.ts │ │ ├── database.d.ts │ │ └── playlist.d.ts │ ├── downloader.d.ts │ ├── tips.d.ts │ ├── mpvIPC.d.ts │ ├── online.d.ts │ ├── files.d.ts │ ├── streamerFiles.d.ts │ ├── binChecker.d.ts │ ├── electron.d.ts │ ├── session.d.ts │ ├── user.d.ts │ ├── backgrounds.d.ts │ ├── database.d.ts │ ├── git.d.ts │ ├── poll.d.ts │ ├── stats.d.ts │ ├── playlist.d.ts │ ├── favorites.d.ts │ ├── repo.d.ts │ └── download.d.ts ├── controllers │ ├── common.ts │ ├── frontend │ │ ├── emulate.ts │ │ ├── test.ts │ │ ├── importBase.ts │ │ ├── backgrounds.ts │ │ └── poll.ts │ └── middlewaresHTTP.ts ├── dao │ ├── sql │ │ ├── migrationsFrontend.ts │ │ ├── upvote.ts │ │ ├── stats.ts │ │ ├── favorites.ts │ │ ├── download.ts │ │ └── session.ts │ ├── stats.ts │ ├── migrationsFrontend.ts │ ├── upvote.ts │ ├── karafile.ts │ ├── favorites.ts │ ├── tagfile.ts │ └── session.ts ├── services │ └── migrationsFrontend.ts ├── utils │ ├── qrcode.ts │ ├── displays.ts │ ├── logger.ts │ └── tips.ts └── electron │ ├── menus │ ├── view.ts │ ├── tools.ts │ ├── goTo.ts │ ├── edit.ts │ └── options.ts │ ├── electronLogger.ts │ └── electronMenu.ts ├── .yarnrc.yml ├── assets ├── blank.png ├── backgrounds │ └── default.jpg └── input.conf ├── img ├── presentation.png └── Logo-final-fond-transparent.png ├── initpage ├── public │ ├── km-logo.png │ ├── nanami-XD.png │ ├── nanami-hehe2.png │ ├── nanami-surpris.png │ └── nanami-searching.gif └── index.html ├── .prettierrc.json ├── .mocharc.json ├── .husky └── pre-commit ├── testUnit └── vitest.config.ts ├── test ├── util │ └── hooks.ts ├── poll.ts ├── player.ts └── auth.ts ├── .gitmodules ├── postgrator.sample.json ├── util ├── triggerWebsite.sh ├── pushKMRemote.sh ├── electronBuilder.sh ├── gitPush.sh ├── replaceVersion.sh ├── cleanupReleases.sh ├── updateFlatpak.sh ├── i18next-scanner.config.cjs ├── socketClient.js ├── versionUtil.sh ├── extUnaccent.js ├── sentryUpdateReleases.js ├── esbuild.js └── changeUserPassword.js ├── .editorconfig ├── .env.example ├── .gitattributes ├── config.CICD.yml ├── .gitlab ├── issue_templates │ └── Bug.md └── sast-ruleset.toml ├── .prettierignore ├── .gitignore ├── LICENSE.md └── tsconfig.json /portable: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /locales/ko.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /kmfrontend/src/locales/ko.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /migrations/20190821192135.do.removeStatsView.sql: -------------------------------------------------------------------------------- 1 | DROP VIEW stats; -------------------------------------------------------------------------------- /migrations/20250618000000.do.betterSSHKeys.sql: -------------------------------------------------------------------------------- 1 | --Dummy migration. -------------------------------------------------------------------------------- /migrations/20200122135323.do.removeRepoTable.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE repo; 2 | -------------------------------------------------------------------------------- /migrations/20201202000000.do.removePLMediasTable.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE pl_medias; -------------------------------------------------------------------------------- /kmfrontend/src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /migrations/20230822000000.do.removeGain.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE kara DROP COLUMN gain; -------------------------------------------------------------------------------- /migrations/20250731000000.do.branchRepositoryIsNowMandatory.sql: -------------------------------------------------------------------------------- 1 | --Dummy migration. -------------------------------------------------------------------------------- /kmfrontend/src/frontend/types/scope.d.ts: -------------------------------------------------------------------------------- 1 | export type Scope = 'admin' | 'public'; 2 | -------------------------------------------------------------------------------- /migrations/20200606000000.do.dropURLDownloads.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE download DROP COLUMN urls; -------------------------------------------------------------------------------- /src/types/frontend.d.ts: -------------------------------------------------------------------------------- 1 | export type WebappModes = 'open' | 'limited' | 'closed'; 2 | -------------------------------------------------------------------------------- /.yarnrc.yml: -------------------------------------------------------------------------------- 1 | nodeLinker: node-modules 2 | 3 | yarnPath: .yarn/releases/yarn-4.9.2.cjs 4 | -------------------------------------------------------------------------------- /migrations/20210222000000.do.dropUserFlagOnline.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE users DROP COLUMN flag_online; -------------------------------------------------------------------------------- /migrations/20220508000000.do.removeYearsView.sql: -------------------------------------------------------------------------------- 1 | DROP MATERIALIZED VIEW IF EXISTS all_years; -------------------------------------------------------------------------------- /src/types/feeds.d.ts: -------------------------------------------------------------------------------- 1 | export interface Feed { 2 | name: string; 3 | body: any; 4 | } 5 | -------------------------------------------------------------------------------- /assets/blank.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/karaokemugen/karaokemugen-app/HEAD/assets/blank.png -------------------------------------------------------------------------------- /img/presentation.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/karaokemugen/karaokemugen-app/HEAD/img/presentation.png -------------------------------------------------------------------------------- /migrations/20200803000000.do.addEndedAtToSessions.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE session ADD COLUMN ended_at TIMESTAMPTZ; -------------------------------------------------------------------------------- /migrations/20210510000000.do.addLocation.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE users ADD COLUMN location CHARACTER VARYING; 2 | -------------------------------------------------------------------------------- /migrations/20220507000000.do.OneMoreGuest.sql: -------------------------------------------------------------------------------- 1 | DELETE FROM users WHERE nickname = 'No bully please'; 2 | -------------------------------------------------------------------------------- /migrations/20221118000000.do.removeLangFromGuestAccount.sql: -------------------------------------------------------------------------------- 1 | UPDATE users SET language = NULL WHERE type = 2 ; -------------------------------------------------------------------------------- /migrations/20230831000000.do.alsoRemoveGainInAllKaras.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE all_karas DROP COLUMN IF EXISTS gain; -------------------------------------------------------------------------------- /kmfrontend/.yarnrc: -------------------------------------------------------------------------------- 1 | --install.ignore-platform true 2 | --add.ignore-platform true 3 | --remove.ignore-platform true -------------------------------------------------------------------------------- /migrations/20190214102323.do.addDefaultNullKaraOrder.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE kara ALTER COLUMN songorder SET DEFAULT NULL; -------------------------------------------------------------------------------- /migrations/20210221000000.do.revertFlagAutoSortByLike.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE playlist DROP COLUMN flag_autosortbylike; -------------------------------------------------------------------------------- /migrations/20210518000000.do.addSendStatsToUsers.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE users ADD COLUMN flag_sendstats BOOLEAN; 2 | -------------------------------------------------------------------------------- /migrations/20210524000000.do.updateGuestsSendStats.sql: -------------------------------------------------------------------------------- 1 | UPDATE users SET flag_sendstats = true 2 | WHERE type = 2; -------------------------------------------------------------------------------- /migrations/20210821000001.do.addLanguageToProfile.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE users ADD COLUMN IF NOT EXISTS language TEXT; -------------------------------------------------------------------------------- /kmfrontend/src/frontend/components/modals/PlayCurrentModal.scss: -------------------------------------------------------------------------------- 1 | .important-name { 2 | font-weight: bold; 3 | } 4 | -------------------------------------------------------------------------------- /migrations/20190515144041.do.removeSubfileNotNullConstraint.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE kara ALTER COLUMN subfile DROP NOT NULL; -------------------------------------------------------------------------------- /migrations/20190730142823.do.wipeBlacklistCriterias.sql: -------------------------------------------------------------------------------- 1 | DELETE FROM blacklist_criteria WHERE type > 0 AND type < 1000; -------------------------------------------------------------------------------- /migrations/20210302000000.do.addKMOnlineMigration.sql: -------------------------------------------------------------------------------- 1 | insert into migrations_frontend values ('KMOnline', false); 2 | -------------------------------------------------------------------------------- /migrations/20210821000000.do.dropSeriesLangMode.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE users DROP COLUMN IF EXISTS series_lang_mode; 2 | -------------------------------------------------------------------------------- /migrations/20220228000000.do.addDescriptionToTags.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE tag ADD COLUMN IF NOT EXISTS description JSONB; 2 | -------------------------------------------------------------------------------- /migrations/20220730000000.do.addExternalDatabaseIds.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE tag ADD COLUMN external_database_ids JSONB; 2 | -------------------------------------------------------------------------------- /src/types/database/upvote.d.ts: -------------------------------------------------------------------------------- 1 | export interface DBUpvote { 2 | username: string; 3 | nickname: string; 4 | } 5 | -------------------------------------------------------------------------------- /src/types/database/user.d.ts: -------------------------------------------------------------------------------- 1 | export interface RemoteToken { 2 | token: string; 3 | username: string; 4 | } 5 | -------------------------------------------------------------------------------- /initpage/public/km-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/karaokemugen/karaokemugen-app/HEAD/initpage/public/km-logo.png -------------------------------------------------------------------------------- /initpage/public/nanami-XD.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/karaokemugen/karaokemugen-app/HEAD/initpage/public/nanami-XD.png -------------------------------------------------------------------------------- /kmfrontend/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/karaokemugen/karaokemugen-app/HEAD/kmfrontend/public/favicon.ico -------------------------------------------------------------------------------- /kmfrontend/src/frontend/components/modals/PollModal.scss: -------------------------------------------------------------------------------- 1 | .modal button.poll { 2 | border-color: #82828226; 3 | } 4 | -------------------------------------------------------------------------------- /migrations/20220613000000.do.addTemporaryFlagToUsers.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE users ADD COLUMN flag_temporary BOOLEAN DEFAULT(false); -------------------------------------------------------------------------------- /migrations/20241103000000.do.addSongname.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE kara ADD COLUMN IF NOT EXISTS songname CHARACTER VARYING; 2 | -------------------------------------------------------------------------------- /migrations/20250515000000.do.recreateSongname.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE IF EXISTS all_karas ADD COLUMN IF NOT EXISTS songname TEXT ; -------------------------------------------------------------------------------- /assets/backgrounds/default.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/karaokemugen/karaokemugen-app/HEAD/assets/backgrounds/default.jpg -------------------------------------------------------------------------------- /kmfrontend/src/assets/Klogo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/karaokemugen/karaokemugen-app/HEAD/kmfrontend/src/assets/Klogo.png -------------------------------------------------------------------------------- /kmfrontend/src/assets/blank.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/karaokemugen/karaokemugen-app/HEAD/kmfrontend/src/assets/blank.png -------------------------------------------------------------------------------- /kmfrontend/src/assets/dame.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/karaokemugen/karaokemugen-app/HEAD/kmfrontend/src/assets/dame.jpg -------------------------------------------------------------------------------- /kmfrontend/src/assets/noise.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/karaokemugen/karaokemugen-app/HEAD/kmfrontend/src/assets/noise.png -------------------------------------------------------------------------------- /locales/nb_NO.json: -------------------------------------------------------------------------------- 1 | { 2 | "NEXT_SONG": "Neste spor", 3 | "NO": "Nei", 4 | "YES": "Ja", 5 | "CANCEL": "Avbryt" 6 | } 7 | -------------------------------------------------------------------------------- /locales/ru.json: -------------------------------------------------------------------------------- 1 | { 2 | "CANCEL": "Отменить", 3 | "YES": "Да", 4 | "NO": "Нет", 5 | "UNKNOWN": "Неизвестный" 6 | } 7 | -------------------------------------------------------------------------------- /migrations/20200203133537.do.addKIDtoDownloads.sql: -------------------------------------------------------------------------------- 1 | TRUNCATE download; 2 | ALTER TABLE download ADD COLUMN kid UUID NOT NULL; -------------------------------------------------------------------------------- /migrations/20210509231128.do.migrateBLC.sql: -------------------------------------------------------------------------------- 1 | update blacklist_criteria set type = 1005 where type = 0 and value !~ '^\d+$'; 2 | -------------------------------------------------------------------------------- /migrations/20220331230559.do.addCollectionsMigration.sql: -------------------------------------------------------------------------------- 1 | INSERT INTO migrations_frontend VALUES ('Collections', false); 2 | -------------------------------------------------------------------------------- /migrations/20240325000000.do.moveInstanceIDAndTokenFromDBToConfig.sql: -------------------------------------------------------------------------------- 1 | -- Do nothing here. We'll take care of it in migrations.ts -------------------------------------------------------------------------------- /migrations/20241215000000.do.FixMigrateLyricInfos.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE all_karas 2 | ADD COLUMN IF NOT EXISTS lyrics_infos jsonb; -------------------------------------------------------------------------------- /migrations/20250330000000.do.addPlayedAtPlaylist.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE playlist_content ADD COLUMN IF NOT EXISTS played_at TIMESTAMPTZ; -------------------------------------------------------------------------------- /initpage/public/nanami-hehe2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/karaokemugen/karaokemugen-app/HEAD/initpage/public/nanami-hehe2.png -------------------------------------------------------------------------------- /initpage/public/nanami-surpris.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/karaokemugen/karaokemugen-app/HEAD/initpage/public/nanami-surpris.png -------------------------------------------------------------------------------- /kmfrontend/src/assets/nanami.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/karaokemugen/karaokemugen-app/HEAD/kmfrontend/src/assets/nanami.png -------------------------------------------------------------------------------- /kmfrontend/src/assets/nanami.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/karaokemugen/karaokemugen-app/HEAD/kmfrontend/src/assets/nanami.webp -------------------------------------------------------------------------------- /migrations/20210430000000.do.addKIDtoDownloads.sql: -------------------------------------------------------------------------------- 1 | TRUNCATE TABLE download; 2 | ALTER TABLE download ADD COLUMN fk_kid UUID NOT NULL; -------------------------------------------------------------------------------- /migrations/20210508000000.do.addBulldozerVideosMigration.sql: -------------------------------------------------------------------------------- 1 | insert into migrations_frontend values ('BulldozerVideos', false); 2 | -------------------------------------------------------------------------------- /migrations/20220516000000.do.deprecateCriterias1004and1005.sql: -------------------------------------------------------------------------------- 1 | DELETE FROM playlist_criteria 2 | WHERE type = 1004 OR type = 1005; -------------------------------------------------------------------------------- /img/Logo-final-fond-transparent.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/karaokemugen/karaokemugen-app/HEAD/img/Logo-final-fond-transparent.png -------------------------------------------------------------------------------- /initpage/public/nanami-searching.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/karaokemugen/karaokemugen-app/HEAD/initpage/public/nanami-searching.gif -------------------------------------------------------------------------------- /kmfrontend/src/assets/nanami-cry.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/karaokemugen/karaokemugen-app/HEAD/kmfrontend/src/assets/nanami-cry.png -------------------------------------------------------------------------------- /kmfrontend/src/assets/nanami-umu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/karaokemugen/karaokemugen-app/HEAD/kmfrontend/src/assets/nanami-umu.png -------------------------------------------------------------------------------- /src/types/database/migrationsFrontend.d.ts: -------------------------------------------------------------------------------- 1 | export interface MigrationsFrontend { 2 | name: string; 3 | flag_done: boolean; 4 | } 5 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "useTabs": true, 4 | "arrowParens": "avoid", 5 | "trailingComma": "es5" 6 | } 7 | -------------------------------------------------------------------------------- /kmfrontend/src/assets/nanami-cry.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/karaokemugen/karaokemugen-app/HEAD/kmfrontend/src/assets/nanami-cry.webp -------------------------------------------------------------------------------- /kmfrontend/src/assets/nanami-hehe2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/karaokemugen/karaokemugen-app/HEAD/kmfrontend/src/assets/nanami-hehe2.png -------------------------------------------------------------------------------- /kmfrontend/src/assets/nanami-sing.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/karaokemugen/karaokemugen-app/HEAD/kmfrontend/src/assets/nanami-sing.png -------------------------------------------------------------------------------- /kmfrontend/src/assets/nanami-sing.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/karaokemugen/karaokemugen-app/HEAD/kmfrontend/src/assets/nanami-sing.webp -------------------------------------------------------------------------------- /kmfrontend/src/assets/nanami-smile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/karaokemugen/karaokemugen-app/HEAD/kmfrontend/src/assets/nanami-smile.png -------------------------------------------------------------------------------- /kmfrontend/src/assets/nanami-smile.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/karaokemugen/karaokemugen-app/HEAD/kmfrontend/src/assets/nanami-smile.webp -------------------------------------------------------------------------------- /kmfrontend/src/assets/nanami-think.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/karaokemugen/karaokemugen-app/HEAD/kmfrontend/src/assets/nanami-think.png -------------------------------------------------------------------------------- /kmfrontend/src/assets/nanami-think.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/karaokemugen/karaokemugen-app/HEAD/kmfrontend/src/assets/nanami-think.webp -------------------------------------------------------------------------------- /kmfrontend/src/assets/nanami-umu.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/karaokemugen/karaokemugen-app/HEAD/kmfrontend/src/assets/nanami-umu.webp -------------------------------------------------------------------------------- /kmfrontend/src/assets/tuto_karaline.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/karaokemugen/karaokemugen-app/HEAD/kmfrontend/src/assets/tuto_karaline.png -------------------------------------------------------------------------------- /migrations/20190620101811.do.addDownloadBLCUniqueValue.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE download_blacklist_criteria ADD COLUMN uniquevalue CHARACTER VARYING; -------------------------------------------------------------------------------- /migrations/20210216000000.do.addFlagAutoSortByLikeToPlaylistTable.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE playlist ADD COLUMN flag_autosortbylike BOOLEAN DEFAULT false; -------------------------------------------------------------------------------- /kmfrontend/src/assets/nanami-shocked.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/karaokemugen/karaokemugen-app/HEAD/kmfrontend/src/assets/nanami-shocked.png -------------------------------------------------------------------------------- /kmfrontend/src/assets/nanami-shocked.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/karaokemugen/karaokemugen-app/HEAD/kmfrontend/src/assets/nanami-shocked.webp -------------------------------------------------------------------------------- /kmfrontend/src/assets/nanami-surpris.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/karaokemugen/karaokemugen-app/HEAD/kmfrontend/src/assets/nanami-surpris.png -------------------------------------------------------------------------------- /kmfrontend/src/frontend/components/modals/CropAvatarModal.scss: -------------------------------------------------------------------------------- 1 | .crop-avatar { 2 | max-height: 60vh !important; 3 | height: auto; 4 | } 5 | -------------------------------------------------------------------------------- /kmfrontend/.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "useTabs": true, 4 | "arrowParens": "avoid", 5 | "trailingComma": "es5" 6 | } 7 | -------------------------------------------------------------------------------- /kmfrontend/src/assets/Logo-fond-transp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/karaokemugen/karaokemugen-app/HEAD/kmfrontend/src/assets/Logo-fond-transp.png -------------------------------------------------------------------------------- /kmfrontend/src/assets/nanami-searching.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/karaokemugen/karaokemugen-app/HEAD/kmfrontend/src/assets/nanami-searching.gif -------------------------------------------------------------------------------- /src/types/downloader.d.ts: -------------------------------------------------------------------------------- 1 | export interface DownloadItem { 2 | url: string; 3 | filename: string; 4 | size?: number; 5 | id?: string; 6 | } 7 | -------------------------------------------------------------------------------- /kmfrontend/src/frontend/components/modals/OnlineProfileModal.scss: -------------------------------------------------------------------------------- 1 | .warnDeleteOnlineAccount { 2 | font-size: larger; 3 | margin-bottom: 0.5em; 4 | } 5 | -------------------------------------------------------------------------------- /migrations/20220504000000.do.removeMoreGuests.sql: -------------------------------------------------------------------------------- 1 | DELETE FROM users WHERE nickname = 'Le Respect'; 2 | DELETE FROM users WHERE nickname = 'No Bully Please'; -------------------------------------------------------------------------------- /.mocharc.json: -------------------------------------------------------------------------------- 1 | { 2 | "node-option": ["loader=ts-node/esm"], 3 | "require": "test/util/hooks.ts", 4 | "timeout": 60000, 5 | "spec": "test/*.ts" 6 | } 7 | -------------------------------------------------------------------------------- /migrations/20191212100536.do.addPrivateFlagForSessions.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE session ADD COLUMN private BOOLEAN DEFAULT FALSE; 2 | UPDATE session SET private = FALSE; -------------------------------------------------------------------------------- /migrations/20221114000000.do.addSongtypeSongorderToSearchVector.sql: -------------------------------------------------------------------------------- 1 | -- This migration has been replaced with the next one fixPreviousMigration because it was bugged. -------------------------------------------------------------------------------- /src/types/tips.d.ts: -------------------------------------------------------------------------------- 1 | export interface TipsAndTricks { 2 | normal: string[]; 3 | errors: string[]; 4 | } 5 | 6 | export type TipType = 'normal' | 'errors'; 7 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | LIB_GIT_INDEX_FILE="$(pwd)/.git/modules/src/lib/index" 2 | 3 | GIT_INDEX_FILE=$LIB_GIT_INDEX_FILE npx lint-staged --cwd src/lib 4 | npx lint-staged 5 | -------------------------------------------------------------------------------- /kmfrontend/src/assets/Logo-final-fond-transparent.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/karaokemugen/karaokemugen-app/HEAD/kmfrontend/src/assets/Logo-final-fond-transparent.png -------------------------------------------------------------------------------- /migrations/20191215162839.do.makeNicknameMandatory.sql: -------------------------------------------------------------------------------- 1 | UPDATE users SET nickname = pk_login WHERE nickname = NULL; 2 | ALTER TABLE users ALTER COLUMN nickname SET NOT NULL; -------------------------------------------------------------------------------- /migrations/20210719000000.do.removeSubchecksum.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE kara DROP COLUMN IF EXISTS subchecksum; 2 | ALTER TABLE all_karas DROP COLUMN IF EXISTS subchecksum; 3 | -------------------------------------------------------------------------------- /testUnit/vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vitest/config'; 2 | 3 | export default defineConfig({ 4 | test: { 5 | dir: 'testUnit', 6 | }, 7 | }); 8 | -------------------------------------------------------------------------------- /kmfrontend/src/frontend/styles/fonts/Lato/Lato-Bold.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/karaokemugen/karaokemugen-app/HEAD/kmfrontend/src/frontend/styles/fonts/Lato/Lato-Bold.eot -------------------------------------------------------------------------------- /kmfrontend/src/frontend/styles/fonts/Lato/Lato-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/karaokemugen/karaokemugen-app/HEAD/kmfrontend/src/frontend/styles/fonts/Lato/Lato-Bold.ttf -------------------------------------------------------------------------------- /kmfrontend/src/frontend/styles/fonts/Lato/Lato-Bold.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/karaokemugen/karaokemugen-app/HEAD/kmfrontend/src/frontend/styles/fonts/Lato/Lato-Bold.woff -------------------------------------------------------------------------------- /kmfrontend/src/frontend/styles/fonts/Lato/Lato-Light.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/karaokemugen/karaokemugen-app/HEAD/kmfrontend/src/frontend/styles/fonts/Lato/Lato-Light.eot -------------------------------------------------------------------------------- /kmfrontend/src/frontend/styles/fonts/Lato/Lato-Light.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/karaokemugen/karaokemugen-app/HEAD/kmfrontend/src/frontend/styles/fonts/Lato/Lato-Light.ttf -------------------------------------------------------------------------------- /migrations/20201206000000.do.addTutorialDoneFlagToUsers.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE users ADD COLUMN flag_tutorial_done BOOLEAN DEFAULT false; 2 | UPDATE users SET flag_tutorial_done = true; -------------------------------------------------------------------------------- /src/types/database/download.d.ts: -------------------------------------------------------------------------------- 1 | import { KaraDownload } from '../download.js'; 2 | 3 | export interface DBDownload extends KaraDownload { 4 | started_at: Date; 5 | } 6 | -------------------------------------------------------------------------------- /kmfrontend/src/frontend/styles/al-icons/font/al-icons.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/karaokemugen/karaokemugen-app/HEAD/kmfrontend/src/frontend/styles/al-icons/font/al-icons.eot -------------------------------------------------------------------------------- /kmfrontend/src/frontend/styles/al-icons/font/al-icons.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/karaokemugen/karaokemugen-app/HEAD/kmfrontend/src/frontend/styles/al-icons/font/al-icons.ttf -------------------------------------------------------------------------------- /kmfrontend/src/frontend/styles/al-icons/font/al-icons.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/karaokemugen/karaokemugen-app/HEAD/kmfrontend/src/frontend/styles/al-icons/font/al-icons.woff -------------------------------------------------------------------------------- /kmfrontend/src/frontend/styles/al-icons/font/al-icons.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/karaokemugen/karaokemugen-app/HEAD/kmfrontend/src/frontend/styles/al-icons/font/al-icons.woff2 -------------------------------------------------------------------------------- /kmfrontend/src/frontend/styles/fonts/Lato/Lato-Bold.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/karaokemugen/karaokemugen-app/HEAD/kmfrontend/src/frontend/styles/fonts/Lato/Lato-Bold.woff2 -------------------------------------------------------------------------------- /kmfrontend/src/frontend/styles/fonts/Lato/Lato-Light.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/karaokemugen/karaokemugen-app/HEAD/kmfrontend/src/frontend/styles/fonts/Lato/Lato-Light.woff -------------------------------------------------------------------------------- /kmfrontend/src/frontend/styles/fonts/Lato/Lato-Light.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/karaokemugen/karaokemugen-app/HEAD/kmfrontend/src/frontend/styles/fonts/Lato/Lato-Light.woff2 -------------------------------------------------------------------------------- /kmfrontend/src/frontend/styles/fonts/Lato/Lato-Regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/karaokemugen/karaokemugen-app/HEAD/kmfrontend/src/frontend/styles/fonts/Lato/Lato-Regular.eot -------------------------------------------------------------------------------- /kmfrontend/src/frontend/styles/fonts/Lato/Lato-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/karaokemugen/karaokemugen-app/HEAD/kmfrontend/src/frontend/styles/fonts/Lato/Lato-Regular.ttf -------------------------------------------------------------------------------- /kmfrontend/src/frontend/styles/fonts/Lato/Lato-Regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/karaokemugen/karaokemugen-app/HEAD/kmfrontend/src/frontend/styles/fonts/Lato/Lato-Regular.woff -------------------------------------------------------------------------------- /kmfrontend/src/frontend/styles/fonts/Lato/Lato-Regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/karaokemugen/karaokemugen-app/HEAD/kmfrontend/src/frontend/styles/fonts/Lato/Lato-Regular.woff2 -------------------------------------------------------------------------------- /migrations/20190525113043.do.addFlagVisibleColumnToPLC.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE playlist_content ADD COLUMN flag_visible BOOLEAN DEFAULT true; 2 | UPDATE playlist_content SET flag_visible = true; -------------------------------------------------------------------------------- /migrations/20211009000000.do.fixCriteriasInPLCTable.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE playlist_content DROP COLUMN IF EXISTS criterias; 2 | ALTER TABLE playlist_content ADD COLUMN criterias jsonb[]; 3 | -------------------------------------------------------------------------------- /migrations/20211010000001.do.addFlagParentsOnlyToUsers.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE users ADD COLUMN IF NOT EXISTS flag_parentsonly BOOLEAN DEFAULT(TRUE); 2 | UPDATE users SET flag_parentsonly = TRUE; -------------------------------------------------------------------------------- /migrations/20200627000000.do.assFullIndexPlayed.sql: -------------------------------------------------------------------------------- 1 | DROP INDEX idx_played_startedat_kid_playedat; 2 | CREATE UNIQUE INDEX idx_played_seid_kid_playedat ON played (fk_kid, played_at, fk_seid); -------------------------------------------------------------------------------- /migrations/20210904000000.do.removeCommentAddCriteria.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE playlist_content ADD COLUMN IF NOT EXISTS criterias JSONB; 2 | ALTER TABLE playlist_content DROP COLUMN IF EXISTS comment; -------------------------------------------------------------------------------- /src/types/mpvIPC.d.ts: -------------------------------------------------------------------------------- 1 | export interface MpvCommand { 2 | command: any[]; 3 | request_id?: number; 4 | } 5 | 6 | export type MpvHardwareDecodingOptions = 'auto-safe' | 'no' | 'yes'; 7 | -------------------------------------------------------------------------------- /migrations/20220607000000.do.aBarrelRoll.sql: -------------------------------------------------------------------------------- 1 | --This migration actually does nothing and is used to trigger the config change needed for #1256. 2 | --Sue me, my lawyers will have you for breakfast. -------------------------------------------------------------------------------- /migrations/20231029000000.do.addFromDisplayType.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE kara ADD COLUMN IF NOT EXISTS from_display_type TEXT; 2 | ALTER TABLE all_karas ADD COLUMN IF NOT EXISTS from_display_type TEXT; 3 | -------------------------------------------------------------------------------- /src/types/online.d.ts: -------------------------------------------------------------------------------- 1 | export interface OnlineForm { 2 | IP6Prefix?: string; 3 | localIP4?: string; 4 | localPort: number; 5 | IP4?: string; 6 | IP6?: string; 7 | IID: string; 8 | } 9 | -------------------------------------------------------------------------------- /test/util/hooks.ts: -------------------------------------------------------------------------------- 1 | import { disconnectSocket } from './util.js'; 2 | 3 | export const mochaHooks = { 4 | afterAll(done: any) { 5 | disconnectSocket(); 6 | done(); 7 | }, 8 | }; 9 | -------------------------------------------------------------------------------- /kmfrontend/src/frontend/types/news.d.ts: -------------------------------------------------------------------------------- 1 | export interface News { 2 | html: string; 3 | date: string; 4 | dateStr: string; 5 | title: string; 6 | link: string; 7 | type: string; 8 | } 9 | -------------------------------------------------------------------------------- /kmfrontend/src/frontend/types/tag.d.ts: -------------------------------------------------------------------------------- 1 | export interface Tag { 2 | type: Array; 3 | value: string; 4 | label: string; 5 | karacount: { count: number; type: number }[]; 6 | } 7 | -------------------------------------------------------------------------------- /kmfrontend/src/frontend/global.d.ts: -------------------------------------------------------------------------------- 1 | // fix error trying to import gif animations 2 | declare module '*.gif' { 3 | const src: string; // Or any type you want to use 4 | export default src; 5 | } 6 | -------------------------------------------------------------------------------- /src/controllers/common.ts: -------------------------------------------------------------------------------- 1 | import logger from '../lib/utils/logger.js'; 2 | 3 | export function errMessage(code: string, message?: any) { 4 | logger.error(`${code}`, { service: 'API', obj: message }); 5 | } 6 | -------------------------------------------------------------------------------- /src/types/files.d.ts: -------------------------------------------------------------------------------- 1 | export type KMFileType = 2 | | 'Karaoke Mugen Karaoke Bundle File' 3 | | 'Karaoke Mugen Karaoke Data File' 4 | | 'Karaoke Mugen Favorites List File' 5 | | 'Karaoke Mugen Playlist File'; 6 | -------------------------------------------------------------------------------- /migrations/20200123153420.do.addRepoToDownload.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE download ADD COLUMN repository CHARACTER VARYING; 2 | UPDATE download SET repository = 'kara.moe'; 3 | ALTER TABLE download ALTER COLUMN repository SET NOT NULL; -------------------------------------------------------------------------------- /migrations/20210127000000.do.addMigrationFrontendTable.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE migrations_frontend ( 2 | name CHARACTER VARYING, 3 | flag_done BOOLEAN 4 | ); 5 | 6 | INSERT INTO migrations_frontend VALUES ('privacyPolicy', false); -------------------------------------------------------------------------------- /kmfrontend/src/frontend/types/playlist.d.ts: -------------------------------------------------------------------------------- 1 | interface PlaylistElem { 2 | plaid: string; 3 | name: string; 4 | karacount?: number; 5 | flag_current?: boolean; 6 | flag_public?: boolean; 7 | flag_visible?: boolean; 8 | } 9 | -------------------------------------------------------------------------------- /migrations/20210118000000.do.addOnlineRequestedTable.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE online_requested ( 2 | fk_kid uuid NOT NULL, 3 | requested integer DEFAULT(0) 4 | ); 5 | 6 | CREATE INDEX idx_online_requested_kid ON online_requested (fk_kid); -------------------------------------------------------------------------------- /migrations/20221001000000.do.addAnimeList.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE users ADD COLUMN anime_list_to_fetch text; 2 | ALTER TABLE users ADD COLUMN anime_list_last_modified_at timestamp with time zone; 3 | ALTER TABLE users ADD COLUMN anime_list_ids int[]; 4 | -------------------------------------------------------------------------------- /migrations/20251001000000.do.changeKaraRelationConstraints.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE kara_relation DROP CONSTRAINT kara_relation_fk_kid_parent_fkey; 2 | 3 | ALTER TABLE kara_relation ALTER COLUMN fk_kid_parent SET DEFAUlT '00000000-0000-0000-0000-000000000000'; -------------------------------------------------------------------------------- /kmfrontend/src/frontend/styles/components/overlays.scss: -------------------------------------------------------------------------------- 1 | @use '../variables'; 2 | 3 | .overlay { 4 | background-color: variables.$mugen-video-overlay; 5 | position: absolute; 6 | top: 0; 7 | right: 0; 8 | bottom: 0; 9 | left: 0; 10 | } 11 | -------------------------------------------------------------------------------- /src/types/streamerFiles.d.ts: -------------------------------------------------------------------------------- 1 | export type StreamFileType = 2 | | 'song_name' 3 | | 'requester' 4 | | 'next_song_name_and_requester' 5 | | 'km_url' 6 | | 'frontend_state' 7 | | 'public_kara_count' 8 | | 'player_status' 9 | | 'current_playlist_info'; 10 | -------------------------------------------------------------------------------- /migrations/20210227000000.do.addFlagAcceptRefuse.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE playlist_content ADD COLUMN flag_accepted BOOLEAN; 2 | ALTER TABLE playlist_content ADD COLUMN flag_refused BOOLEAN; 3 | 4 | UPDATE playlist_content SET 5 | flag_accepted = FALSE, 6 | flag_refused = FALSE; 7 | -------------------------------------------------------------------------------- /kmfrontend/src/systempanel/components/karas/CheckBoxTag.scss: -------------------------------------------------------------------------------- 1 | .checkbox-tag-form .ant-checkbox { 2 | height: 100%; 3 | margin-top: 0.5em; 4 | 5 | .ant-checkbox-inner:after { 6 | top: 0.5em; 7 | } 8 | 9 | .ant-wave { 10 | display: none; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "src/lib"] 2 | path = src/lib 3 | url = ../lib 4 | [submodule "assets/guestAvatars"] 5 | path = assets/guestAvatars 6 | url = ../../medias/guest-avatars 7 | [submodule "assets/systemRepo"] 8 | path = assets/systemRepo 9 | url = ../../bases/system.git 10 | -------------------------------------------------------------------------------- /migrations/20200708000000.do.addMediasTable.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE pl_medias ( 2 | type CHARACTER VARYING NOT NULL, 3 | filename CHARACTER VARYING NOT NULL, 4 | size INTEGER DEFAULT(0), 5 | audiogain REAL DEFAULT(0) 6 | ); 7 | 8 | CREATE UNIQUE INDEX ON pl_medias USING btree(type, filename); -------------------------------------------------------------------------------- /postgrator.sample.json: -------------------------------------------------------------------------------- 1 | { 2 | "migrationDirectory": "migrations", 3 | "driver": "pg", 4 | "host": "localhost", 5 | "port": 5432, 6 | "database": "karaokemugen_app", 7 | "username": "karaokemugen_app", 8 | "password": "musubi", 9 | "validateChecksums": false 10 | } 11 | -------------------------------------------------------------------------------- /util/triggerWebsite.sh: -------------------------------------------------------------------------------- 1 | source util/versionUtil.sh 2 | 3 | curl -X POST -F "variables[RELEASE]=$RELEASE" -F "variables[VERSIONNAME]=$VERSION_NAME" -F "variables[VERSIONNUMBER]=$BUILDVERSION" -F "token=$TRIGGERWEB" -F "ref=master" "https://gitlab.com/api/v4/projects/32123824/trigger/pipeline" 4 | -------------------------------------------------------------------------------- /migrations/20241203000000.do.migrateLyricInfos.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE kara 2 | ADD lyrics_infos jsonb; 3 | 4 | ALTER TABLE kara 5 | DROP COLUMN announce_position_x; 6 | 7 | ALTER TABLE kara 8 | DROP COLUMN announce_position_y; 9 | 10 | ALTER TABLE kara 11 | DROP COLUMN subfile; 12 | -------------------------------------------------------------------------------- /migrations/20250224000000.do.addSongsPlayedAndLeftToPlaylistInfo.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE playlist ADD COLUMN IF NOT EXISTS songs_played INTEGER DEFAULT 0; 2 | ALTER TABLE playlist ADD COLUMN IF NOT EXISTS songs_left INTEGER DEFAULT 0; 3 | ALTER TABLE playlist ADD COLUMN IF NOT EXISTS time_played INTEGER DEFAULT 0; -------------------------------------------------------------------------------- /util/pushKMRemote.sh: -------------------------------------------------------------------------------- 1 | source util/versionUtil.sh 2 | 3 | cd kmfrontend/dist 4 | 5 | zip -r $BUILDVERSION.zip . 6 | 7 | lftp -c "set cmd:fail-exit yes; set ftp:ssl-allow no; open -u $USERNAME,$PASSWORD $KARAOKESMOE; cd www/mugen.karaokes.moe/frontends; set cmd:fail-exit yes; put -e $BUILDVERSION.zip" 8 | -------------------------------------------------------------------------------- /migrations/20190410081710.do.addBLCDownloadTable.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE download_blacklist_criteria ( 2 | pk_id_dl_blcriteria SERIAL PRIMARY KEY, 3 | type INTEGER NOT NULL, 4 | value CHARACTER VARYING NOT NULL 5 | ); 6 | 7 | CREATE UNIQUE INDEX idx_dlblc_type_value ON download_blacklist_criteria(type, value); -------------------------------------------------------------------------------- /src/types/binChecker.d.ts: -------------------------------------------------------------------------------- 1 | export interface BinariesConfig { 2 | ffmpeg: string; 3 | mpv: string; 4 | postgres: string; 5 | patch: string; 6 | postgres_ctl: string; 7 | postgres_dump: string; 8 | postgres_client: string; 9 | git?: string; // Will be used in later releases 10 | } 11 | -------------------------------------------------------------------------------- /src/types/database/stats.d.ts: -------------------------------------------------------------------------------- 1 | interface DBStatsBase { 2 | kid: string; 3 | seid: string; 4 | } 5 | 6 | export interface DBStatsPlayed extends DBStatsBase { 7 | played_at: Date; 8 | } 9 | 10 | export interface DBStatsRequested extends DBStatsBase { 11 | requested_at: Date; 12 | } 13 | -------------------------------------------------------------------------------- /util/electronBuilder.sh: -------------------------------------------------------------------------------- 1 | source util/versionUtil.sh 2 | 3 | ELECTRONBUILDER=$(which electron-builder) 4 | 5 | if [ $(uname) == 'Darwin' ] 6 | then 7 | ELECTRONBUILDER=/opt/homebrew/bin/electron-builder 8 | fi 9 | 10 | $ELECTRONBUILDER $1 $2 --publish always -c.extraMetadata.version=$BUILDVERSION 11 | -------------------------------------------------------------------------------- /kmfrontend/src/frontend/types/image.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.png' { 2 | const value: any; 3 | export = value; 4 | } 5 | 6 | declare module '*.jpg' { 7 | const value: any; 8 | export = value; 9 | } 10 | 11 | declare module '*.webp' { 12 | const value: any; 13 | export = value; 14 | } 15 | -------------------------------------------------------------------------------- /kmfrontend/src/frontend/components/modals/AutoMixModal.scss: -------------------------------------------------------------------------------- 1 | p.autoMixExplanation { 2 | margin: 1em; 3 | } 4 | 5 | ul.autoMixCriteria { 6 | padding: 1em; 7 | } 8 | 9 | .modal-body.automix { 10 | h5 { 11 | font-size: 1.2em; 12 | font-weight: bold; 13 | padding: 0.5em; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /migrations/20190215154455.do.addKaraSerieLangMaterializedView.sql: -------------------------------------------------------------------------------- 1 | CREATE MATERIALIZED VIEW all_kara_serie_langs AS 2 | SELECT sl.name, sl.lang, ks.fk_kid AS kid 3 | FROM serie_lang sl 4 | INNER JOIN kara_serie ks ON sl.fk_sid = ks.fk_sid; 5 | 6 | CREATE INDEX idx_akls_kid_lang ON all_kara_serie_langs(kid, lang); -------------------------------------------------------------------------------- /migrations/20231128000000.do.addAnnouncePositionToKara.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE kara ADD COLUMN IF NOT EXISTS announce_position_x TEXT check (announce_position_x in ('Left', 'Center', 'Right')); 2 | ALTER TABLE kara ADD COLUMN IF NOT EXISTS announce_position_y TEXT check (announce_position_y in ('Top', 'Center', 'Bottom')); 3 | -------------------------------------------------------------------------------- /migrations/20250728000000.do.updatePlaylistServerFields.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE playlist ADD COLUMN IF NOT EXISTS slug TEXT; 2 | ALTER TABLE playlist ADD COLUMN IF NOT EXISTS description TEXT; 3 | ALTER TABLE playlist ADD COLUMN IF NOT EXISTS nickname TEXT; 4 | ALTER TABLE playlist ADD COLUMN IF NOT EXISTS contributors JSONB[]; -------------------------------------------------------------------------------- /migrations/20190617090553.do.removeOldGuestNames.sql: -------------------------------------------------------------------------------- 1 | /* TO MAKE KMEUH HAPPY */ 2 | 3 | DELETE FROM users WHERE pk_login = 'Houonin Kyouma'; 4 | DELETE FROM users WHERE pk_login = 'Bunny-girl sempai'; 5 | DELETE FROM users WHERE pk_login = 'Silent Mobius DVD'; 6 | DELETE FROM users WHERE pk_login = 'Kousei Arima'; 7 | -------------------------------------------------------------------------------- /kmfrontend/src/locales/nl.json: -------------------------------------------------------------------------------- 1 | { 2 | "SELECT_PLACEHOLDER": "Selecteer een afspeellijst", 3 | "YES": "Ja", 4 | "NO": "Nee", 5 | "AND": "en", 6 | "CANCEL": "Annuleren", 7 | "OR": "of", 8 | "UNKNOWN": "Onbekend", 9 | "SEARCH": "Zoeken", 10 | "SUBMIT": "Opslaan", 11 | "CONFIRM": "Bevestigen" 12 | } 13 | -------------------------------------------------------------------------------- /migrations/20210501000000.do.addDownloadedToKaras.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE kara ADD COLUMN download_status character VARYING DEFAULT 'MISSING'; 2 | ALTER TABLE all_karas ADD COLUMN download_status character VARYING DEFAULT 'MISSING'; 3 | UPDATE kara SET download_status = 'MISSING'; 4 | UPDATE all_karas SET download_status = 'MISSING'; -------------------------------------------------------------------------------- /migrations/20240527000001.do.fixSingergroupsSortablePart2.sql: -------------------------------------------------------------------------------- 1 | UPDATE all_karas ak 2 | SET search_vector_parents = search_vector || ( 3 | SELECT tsvector_agg(akp.search_vector) 4 | FROM all_karas akp 5 | LEFT JOIN kara_relation kr ON kr.fk_kid_child = akp.pk_kid 6 | WHERE kr.fk_kid_parent = ak.pk_kid 7 | ); 8 | -------------------------------------------------------------------------------- /migrations/20250530000000.do.decouplingOnlineConfig.sql: -------------------------------------------------------------------------------- 1 | --Nothing happens here, it's a fake migration to trigger code during an update. 2 | 3 | --Maybe I should try to write interesting stuff in these migrations that no one will read 4 | 5 | --Like easter eggs, you know? I like easter eggs. And april fools too. They're fun. 6 | 7 | -------------------------------------------------------------------------------- /src/dao/sql/migrationsFrontend.ts: -------------------------------------------------------------------------------- 1 | export const sqlSelectAllMigrations = 'SELECT name, flag_done FROM migrations_frontend'; 2 | 3 | export const sqlUpdateMigrations = 'UPDATE migrations_frontend SET flag_done = $2 WHERE name = $1'; 4 | 5 | export const sqlMarkAllMigrationsAsDone = 'UPDATE migrations_frontend SET flag_done = true'; 6 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | indent_size = 4 7 | indent_style = tab 8 | insert_final_newline = true 9 | max_line_length = 120 10 | tab_width = 4 11 | 12 | [*.yml] 13 | indent_size = 2 14 | indent_style = space 15 | 16 | [package.json] 17 | indent_size = 2 18 | indent_style = space -------------------------------------------------------------------------------- /kmfrontend/src/store/GlobalStateProvider.tsx: -------------------------------------------------------------------------------- 1 | import GlobalContext from './context'; 2 | import useGlobalState from './useGlobalState'; 3 | 4 | const GlobalStateProvider = ({ children }) => ( 5 | {children} 6 | ); 7 | 8 | export default GlobalStateProvider; 9 | -------------------------------------------------------------------------------- /kmfrontend/src/frontend/types/kara.d.ts: -------------------------------------------------------------------------------- 1 | import { DBPLC } from '../../../../src/lib/types/database/playlist'; 2 | import { Criteria } from '../../../../src/lib/types/playlist'; 3 | 4 | interface KaraElement extends DBPLC { 5 | checked: boolean; 6 | criterias?: Criteria[]; 7 | my_public_plc_id: number[]; 8 | public_plc_id: number[]; 9 | } 10 | -------------------------------------------------------------------------------- /migrations/20190226223300.do.createDownloadTable.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE download ( 2 | pk_uuid uuid NOT NULL PRIMARY KEY, 3 | name character varying NOT NULL, 4 | urls jsonb NOT NULL, 5 | size integer NOT NULL DEFAULT 0, 6 | status character VARYING NOT NULL DEFAULT 'DL_PLANNED', 7 | started_at timestamp NOT NULL DEFAULT now() 8 | ); 9 | 10 | -------------------------------------------------------------------------------- /migrations/20200622000000.do.changeFlagPlaying.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE playlist ADD COLUMN fk_id_plcontent_playing INTEGER NOT NULL DEFAULT 0; 2 | UPDATE playlist SET fk_id_plcontent_playing = COALESCE((SELECT pk_id_plcontent FROM playlist_content WHERE fk_id_playlist = pk_id_playlist AND flag_playing = TRUE), 0); 3 | ALTER TABLE playlist_content DROP COLUMN flag_playing; -------------------------------------------------------------------------------- /migrations/20190524114916.do.addLangModeColumnsUser.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE users ADD COLUMN series_lang_mode INTEGER DEFAULT -1; 2 | UPDATE users SET series_lang_mode = -1; 3 | ALTER TABLE users ADD COLUMN main_series_lang CHARACTER VARYING; 4 | ALTER TABLE users ADD COLUMN fallback_series_lang CHARACTER VARYING; 5 | ALTER TABLE users ALTER COLUMN series_lang_mode SET NOT NULL; -------------------------------------------------------------------------------- /migrations/20210212010000.do.addUniqueIndexToAllTagsAndYearsView.sql: -------------------------------------------------------------------------------- 1 | DROP INDEX idx_at_tid; 2 | 3 | CREATE UNIQUE INDEX idx_at_tid 4 | on all_tags (tid); 5 | 6 | DROP INDEX idx_ay_year; 7 | 8 | CREATE UNIQUE INDEX idx_ay_year ON all_years(year); 9 | 10 | DROP INDEX idx_ak_kid; 11 | 12 | CREATE UNIQUE index idx_ak_kid 13 | on all_karas (kid); -------------------------------------------------------------------------------- /migrations/20210928000000.do.addAndOrSmatPlaylists.sql: -------------------------------------------------------------------------------- 1 | DO $$ 2 | BEGIN 3 | IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'smartPlaylistType') THEN 4 | CREATE TYPE smartPlaylistType AS 5 | ENUM('UNION', 'INTERSECT'); 6 | END IF; 7 | END$$; 8 | ALTER TABLE playlist ADD COLUMN IF NOT EXISTS type_smart smartPlaylistType DEFAULT 'INTERSECT'; -------------------------------------------------------------------------------- /migrations/20201123141624.do.guestsRename.sql: -------------------------------------------------------------------------------- 1 | begin transaction; 2 | delete from users ou 3 | where type = 2 and pk_login in ( 4 | select pk_login from users where type = 2 and 5 | (select count(*) from users inr where type = 2 and lower(inr.pk_login) = lower(ou.pk_login)) > 1 6 | ); 7 | update users set pk_login = lower(pk_login) where type = 2; 8 | commit; 9 | -------------------------------------------------------------------------------- /kmfrontend/src/frontend/styles/components/titles.scss: -------------------------------------------------------------------------------- 1 | .karaTitle { 2 | display: flex; 3 | flex-grow: 1; 4 | align-items: center; 5 | flex-wrap: wrap; 6 | > span { 7 | margin: 0.125em 0; 8 | } 9 | span.tag.inline.white { 10 | padding: 0 0.25em; 11 | margin: 0.125em 0 0.125em 0.5em; 12 | } 13 | > i.fas { 14 | margin-left: 0.375em; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /locales/zh-Hans.json: -------------------------------------------------------------------------------- 1 | { 2 | "NO": "否", 3 | "CANCEL": "取消", 4 | "YES": "是", 5 | "UNKNOWN": "未知", 6 | "YEAR": "年", 7 | "TITLE": "标题", 8 | "LIBRARY": "库", 9 | "TAG_TYPES": { 10 | "CREATORS": "创建者", 11 | "LANGS": "语言", 12 | "GROUPS": "群组", 13 | "PLATFORMS": "平台", 14 | "SERIES": "系列", 15 | "VERSIONS": "版本", 16 | "SINGERS": "歌手" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/types/electron.d.ts: -------------------------------------------------------------------------------- 1 | import { MenuItem, MenuItemConstructorOptions } from 'electron'; 2 | 3 | export type MenuLayout = 'REDUCED' | 'DEFAULT'; 4 | 5 | export interface MenuItemBuilderOptions { 6 | layout: MenuLayout; 7 | isMac: boolean; 8 | } 9 | 10 | export type MenuItemBuilderFunction = (options?: MenuItemBuilderOptions) => MenuItemConstructorOptions | MenuItem; 11 | -------------------------------------------------------------------------------- /kmfrontend/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "KM", 3 | "name": "Karaoke Mugen", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": "./index.html", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /migrations/20220328000000.do.removeDuplicateCriterias.sql: -------------------------------------------------------------------------------- 1 | DELETE FROM playlist_criteria T1 2 | USING playlist_criteria T2 3 | WHERE T1.ctid < T2.ctid --yes, it works, this is an internal, hidden column 4 | AND T1.fk_id_playlist < T2.fk_id_playlist 5 | AND T1.value = T2.value 6 | AND T1.type = T2.type; 7 | 8 | CREATE UNIQUE INDEX ON playlist_criteria USING btree(fk_id_playlist, type, value); -------------------------------------------------------------------------------- /src/types/session.d.ts: -------------------------------------------------------------------------------- 1 | export interface Session { 2 | seid: string; 3 | name: string; 4 | started_at: Date; 5 | ended_at: Date; 6 | played?: number; 7 | requested?: number; 8 | active?: boolean; 9 | private?: boolean; 10 | } 11 | 12 | export interface SessionExports { 13 | played: string; 14 | playedCount: string; 15 | requested: string; 16 | requestedCount: string; 17 | } 18 | -------------------------------------------------------------------------------- /kmfrontend/src/frontend/types/remote.d.ts: -------------------------------------------------------------------------------- 1 | import { RemoteFailure, RemoteSuccess } from '../../../../src/lib/types/remote'; 2 | 3 | interface RemoteStatusInactive { 4 | active: false; 5 | } 6 | 7 | interface RemoteStatusActive { 8 | active: true; 9 | info: RemoteSuccess | RemoteFailure; 10 | token: string; 11 | } 12 | 13 | type RemoteStatusData = RemoteStatusInactive | RemoteStatusActive; 14 | -------------------------------------------------------------------------------- /migrations/20190410125416.do.karaTagRestrictToCascade.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE kara_tag DROP CONSTRAINT kara_tag_fk_id_tag_fkey; 2 | ALTER TABLE kara_tag DROP CONSTRAINT kara_tag_fk_kid_fkey; 3 | ALTER TABLE kara_tag ADD CONSTRAINT kara_tag_fk_id_tag_fkey FOREIGN KEY (fk_id_tag) REFERENCES tag(pk_id_tag) ON DELETE CASCADE; 4 | ALTER TABLE kara_tag ADD CONSTRAINT kara_tag_fk_kid_fkey FOREIGN KEY (fk_kid) REFERENCES kara(pk_kid) ON DELETE CASCADE; -------------------------------------------------------------------------------- /kmfrontend/src/frontend/styles/components/blurred-bg.scss: -------------------------------------------------------------------------------- 1 | @mixin blurred-bg { 2 | --img: none; 3 | &::before { 4 | position: absolute; 5 | top: 0; 6 | left: 0; 7 | width: 100%; 8 | height: 100%; 9 | content: ''; 10 | background-image: var(--img); 11 | background-size: cover; 12 | background-position-y: center; 13 | filter: blur(5px) contrast(75%) brightness(75%) saturate(80%); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/types/user.d.ts: -------------------------------------------------------------------------------- 1 | export interface UserOpts { 2 | admin?: boolean; 3 | createRemote?: boolean; 4 | editRemote?: false | string; // Supply online token 5 | renameUser?: boolean; 6 | noPasswordCheck?: boolean; 7 | skipSecurityCode?: boolean; 8 | } 9 | 10 | export interface Tokens { 11 | token: string; 12 | onlineToken: string; 13 | } 14 | 15 | export interface SingleToken { 16 | token: string; 17 | } 18 | -------------------------------------------------------------------------------- /kmfrontend/src/store/reducers/modal.ts: -------------------------------------------------------------------------------- 1 | import { CloseModal, ModalAction, ModalStore, ShowModal } from '../types/modal'; 2 | 3 | export default function modalReducer(state: ModalStore, action: ShowModal | CloseModal) { 4 | switch (action.type) { 5 | case ModalAction.SHOW_MODAL: 6 | case ModalAction.CLOSE_MODAL: 7 | return { 8 | modal: action.payload?.modal || null, 9 | }; 10 | default: 11 | return state; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/services/migrationsFrontend.ts: -------------------------------------------------------------------------------- 1 | import { selectMigrationsFrontend, updateMigrationsFrontend } from '../dao/migrationsFrontend.js'; 2 | import { MigrationsFrontend } from '../types/database/migrationsFrontend.js'; 3 | 4 | export async function getMigrationsFrontend() { 5 | return selectMigrationsFrontend(); 6 | } 7 | 8 | export async function setMigrationsFrontend(mig: MigrationsFrontend) { 9 | return updateMigrationsFrontend(mig); 10 | } 11 | -------------------------------------------------------------------------------- /kmfrontend/src/frontend/components/modals/KaraMenuModal.scss: -------------------------------------------------------------------------------- 1 | .animate-button-container { 2 | position: relative; 3 | } 4 | 5 | .dropdown-menu > li > div.animate-button-success { 6 | position: absolute; 7 | background-color: forestgreen; 8 | top: 0; 9 | left: 0; 10 | width: 100%; 11 | height: 100%; 12 | clip-path: inset(0% 50%); 13 | transition: clip-path 200ms ease; 14 | &.activate { 15 | clip-path: inset(0% 0%); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/types/backgrounds.d.ts: -------------------------------------------------------------------------------- 1 | import { backgroundTypes } from '../services/backgrounds.js'; 2 | 3 | export type BackgroundType = (typeof backgroundTypes)[number]; 4 | 5 | export interface BackgroundList { 6 | pictures: string[]; 7 | music: string[]; 8 | } 9 | 10 | export interface BackgroundRequest { 11 | type: BackgroundType; 12 | file: T; 13 | } 14 | 15 | export interface BackgroundListRequest { 16 | type: BackgroundType; 17 | } 18 | -------------------------------------------------------------------------------- /migrations/20210427000000.do.gitMigrationDownload.sql: -------------------------------------------------------------------------------- 1 | TRUNCATE TABLE download; 2 | ALTER TABLE download DROP COLUMN kid; 3 | ALTER TABLE download ADD COLUMN mediafile CHARACTER VARYING; 4 | 5 | INSERT INTO blacklist_criteria(type, value, uniquevalue, fk_id_blc_set) 6 | SELECT db.type, db.value, db.uniquevalue, (SELECT pk_id_blc_set FROM blacklist_criteria_set WHERE flag_current = true) FROM download_blacklist_criteria db; 7 | 8 | DROP TABLE download_blacklist_criteria; -------------------------------------------------------------------------------- /util/gitPush.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | git config user.name "Release Bot" 3 | git config user.email "server@karaokes.moe" 4 | git checkout $CI_COMMIT_BRANCH 5 | git pull 6 | bash util/replaceVersion.sh 7 | git add package.json 8 | VERSION=$(grep version\": package.json | awk -F\" {'print $4'}) 9 | git commit -m "🚀 new release $VERSION" 10 | git remote set-url origin "https://project_32123684_bot:$DEPLOY_TOKEN@gitlab.com/karaokemugen/code/karaokemugen-app.git" 11 | git push -------------------------------------------------------------------------------- /kmfrontend/src/frontend/components/decorators/KmAppHeaderDecorator.tsx: -------------------------------------------------------------------------------- 1 | import { ReactNode, ReactNodeArray } from 'react'; 2 | 3 | interface IProps { 4 | children?: ReactNodeArray | ReactNode; 5 | mode: string; 6 | } 7 | 8 | function KmAppHeaderDecorator(props: IProps) { 9 | return ( 10 |
11 | {props.children} 12 |
13 | ); 14 | } 15 | 16 | export default KmAppHeaderDecorator; 17 | -------------------------------------------------------------------------------- /locales/nl.json: -------------------------------------------------------------------------------- 1 | { 2 | "REQUESTED_BY": "Aangevraagd door {{name}}", 3 | "REQUESTED_WITH": "(met {{names}})", 4 | "YES": "Ja", 5 | "REQUESTED_AND_one": "en anderen", 6 | "NO": "Nee", 7 | "REQUESTED_AND_other": "en {{count}} anderen", 8 | "CANCEL": "Annuleren", 9 | "YEAR": "Jaar", 10 | "UNKNOWN": "Onbekend", 11 | "TITLE": "Titel", 12 | "LIBRARY": "Bibliotheek", 13 | "TAG_TYPES": { 14 | "SINGERGROUPS": "Band", 15 | "SINGERS": "Zanger" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/utils/qrcode.ts: -------------------------------------------------------------------------------- 1 | import { resolve } from 'path'; 2 | import { toFile } from 'qrcode'; 3 | 4 | import { resolvedPath } from '../lib/utils/config.js'; 5 | 6 | export async function createQRCodeFile(text: string) { 7 | return new Promise((success, _reject) => { 8 | toFile( 9 | resolve(resolvedPath('Temp'), 'qrcode.png'), 10 | text, 11 | { 12 | scale: 8, 13 | }, 14 | () => { 15 | success(true); 16 | } 17 | ); 18 | }); 19 | } 20 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | # Disable all Sentry integrations 2 | SENTRY_TEST=true 3 | # Set custom Sentry DSN 4 | SENTRY_DSN="" 5 | # Define Sentry environment (https://docs.sentry.io/enriching-error-data/environments/) 6 | SENTRY_ENVIRONMENT="development" 7 | # Use proxy to get electron when yarn install 8 | GLOBAL_AGENT_HTTPS_PROXY= 9 | ELECTRON_GET_USE_PROXY=true 10 | # Create initial user and password for headless installs 11 | INITIAL_USER_NAME=myadmincannotbethiscute 12 | INITIAL_USER_PASSWORD=yesitcan -------------------------------------------------------------------------------- /kmfrontend/src/utils/components/Loading.scss: -------------------------------------------------------------------------------- 1 | .loading-container { 2 | background-color: #404040; 3 | color: white; 4 | font-family: sans-serif; 5 | position: fixed; 6 | z-index: 99; 7 | top: 0; 8 | left: 0; 9 | width: 100%; 10 | height: 100%; 11 | display: flex; 12 | flex-direction: column; 13 | justify-content: center; 14 | align-items: center; 15 | padding: 24px; 16 | box-sizing: border-box; 17 | > span.header { 18 | font-size: 1.75em; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /migrations/20230808000000.do.renameidplcontentToPLCID.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE playlist_content RENAME COLUMN pk_id_plcontent TO pk_plcid; 2 | ALTER TABLE upvote RENAME COLUMN fk_id_plcontent TO fk_plcid; 3 | ALTER TABLE playlist RENAME COLUMN fk_id_plcontent_playing TO fk_plcid_playing; 4 | ALTER TABLE playlist RENAME COLUMN pk_id_playlist TO pk_plaid; 5 | ALTER TABLE playlist_content RENAME COLUMN fk_id_playlist TO fk_plaid; 6 | ALTER TABLE playlist_criteria RENAME COLUMN fk_id_playlist TO fk_plaid; 7 | -------------------------------------------------------------------------------- /migrations/20220117000000.do.KaraParentsConstraints.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE kara_relation DROP CONSTRAINT kara_relation_fk_kid_child_fkey; 2 | ALTER TABLE kara_relation DROP CONSTRAINT kara_relation_fk_kid_parent_fkey; 3 | 4 | ALTER TABLE kara_relation ADD CONSTRAINT kara_relation_fk_kid_child_fkey FOREIGN KEY(fk_kid_child) REFERENCES kara(pk_kid) ON DELETE CASCADE; 5 | ALTER TABLE kara_relation ADD CONSTRAINT kara_relation_fk_kid_parent_fkey FOREIGN KEY(fk_kid_parent) REFERENCES kara(pk_kid) ON DELETE CASCADE; -------------------------------------------------------------------------------- /kmfrontend/src/frontend/components/PlaylistPage.scss: -------------------------------------------------------------------------------- 1 | .chibi-playlist { 2 | -webkit-app-region: drag; 3 | padding: 1rem; 4 | 5 | .following { 6 | font-size: 1.75em; 7 | font-weight: bold; 8 | } 9 | 10 | .following-li { 11 | margin: 1em 0; 12 | > .title { 13 | display: flex; 14 | align-items: center; 15 | > .title { 16 | font-size: 2em; 17 | font-weight: bold; 18 | } 19 | } 20 | .series { 21 | font-size: 1.25em; 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /kmfrontend/src/store/types/modal.ts: -------------------------------------------------------------------------------- 1 | import { ReactElement } from 'react'; 2 | 3 | export enum ModalAction { 4 | SHOW_MODAL = 'show_modal', 5 | CLOSE_MODAL = 'close_modal', 6 | } 7 | 8 | export interface ShowModal { 9 | type: ModalAction.SHOW_MODAL; 10 | payload: ModalStore; 11 | } 12 | 13 | export interface CloseModal { 14 | type: ModalAction.CLOSE_MODAL; 15 | payload: { modal: null }; 16 | } 17 | 18 | export interface ModalStore { 19 | modal: ReactElement | null; 20 | } 21 | -------------------------------------------------------------------------------- /src/types/database.d.ts: -------------------------------------------------------------------------------- 1 | export interface Settings { 2 | baseChecksum?: string; 3 | lastGeneration?: number; 4 | } 5 | 6 | export interface Query { 7 | sql: string; 8 | params?: any[][]; 9 | } 10 | 11 | export interface LangClause { 12 | main: string; 13 | fallback: string; 14 | } 15 | 16 | export interface WhereClause { 17 | sql: string[]; 18 | params: Record; 19 | } 20 | 21 | export interface PGVersion { 22 | data: number; 23 | bin: number; 24 | } 25 | -------------------------------------------------------------------------------- /src/types/git.d.ts: -------------------------------------------------------------------------------- 1 | import { LogResult, StatusResult } from 'simple-git'; 2 | 3 | export type DiffType = 'equal' | 'modify' | 'add' | 'delete'; 4 | 5 | export interface DiffResult { 6 | type: DiffType; 7 | path: string; 8 | content?: string; 9 | } 10 | 11 | export interface GitOptions { 12 | url: string; 13 | branch: string; 14 | repo: string; 15 | dir: string; 16 | } 17 | 18 | // For KMFrontend 19 | export type GitStatusResult = StatusResult; 20 | export type GitLogResult = LogResult; 21 | -------------------------------------------------------------------------------- /util/replaceVersion.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | VERSION=$(cat package.json | grep version\": | awk -F\" {'print $4'} | awk -F- {'print $1'}) 3 | 4 | MAJORVERSION=$(echo "$VERSION" | awk -F. {'print $1'}) 5 | MIDDLEVERSION=$(echo "$VERSION" | awk -F. {'print $2'}) 6 | MINORVERSION=$(echo "$VERSION" | awk -F. {'print $3'}) 7 | 8 | MINORVERSION=$((MINORVERSION+1)) 9 | 10 | NEWVERSION="$MAJORVERSION.$MIDDLEVERSION.$MINORVERSION" 11 | 12 | sed -ri "s/\"version\": \"([0-9.]+)(-[a-z]+)?\"/\"version\": \"$NEWVERSION\2\"/" package.json -------------------------------------------------------------------------------- /kmfrontend/src/frontend/components/karas/QuizScore.scss: -------------------------------------------------------------------------------- 1 | .userLine { 2 | display: flex; 3 | flex-wrap: wrap; 4 | justify-content: space-between; 5 | height: 100%; 6 | 7 | > div { 8 | flex: 1; 9 | } 10 | 11 | .userNickname { 12 | display: flex; 13 | align-items: center; 14 | } 15 | 16 | .userAnswer { 17 | text-align: center; 18 | padding-left: 1em; 19 | padding-right: 1em; 20 | } 21 | 22 | .userPoints { 23 | line-height: normal; 24 | text-align: right; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /kmfrontend/src/frontend/components/decorators/KmAppBodyDecorator.tsx: -------------------------------------------------------------------------------- 1 | import { ReactNode } from 'react'; 2 | 3 | interface IProps { 4 | children?: ReactNode; 5 | extraClass?: string; 6 | mode: number | string | undefined; 7 | } 8 | 9 | function KmAppBodyDecorator(props: IProps) { 10 | return ( 11 |
12 | {props.children} 13 |
14 | ); 15 | } 16 | 17 | export default KmAppBodyDecorator; 18 | -------------------------------------------------------------------------------- /kmfrontend/src/systempanel/pages/Karas/KaraForm.scss: -------------------------------------------------------------------------------- 1 | .media-info { 2 | .tr { 3 | white-space: nowrap; 4 | } 5 | 6 | .unmet-required { 7 | color: rgb(243, 88, 88); 8 | } 9 | 10 | .unmet-warning { 11 | color: rgb(243, 181, 88); 12 | } 13 | 14 | tr td { 15 | padding-right: 10px; 16 | } 17 | 18 | tr td:last-child { 19 | padding-right: 0; 20 | } 21 | 22 | &.warnings { 23 | padding-top: 20px; 24 | max-width: 350px; // Prevent the text from expanding the card to 100% 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /kmfrontend/src/store/actions/modal.ts: -------------------------------------------------------------------------------- 1 | import { Dispatch, ReactElement } from 'react'; 2 | 3 | import { CloseModal, ModalAction, ShowModal } from '../types/modal'; 4 | 5 | export function showModal(dispatch: Dispatch, modal: ReactElement) { 6 | dispatch({ 7 | type: ModalAction.SHOW_MODAL, 8 | payload: { modal }, 9 | }); 10 | } 11 | 12 | export function closeModal(dispatch: Dispatch) { 13 | dispatch({ 14 | type: ModalAction.CLOSE_MODAL, 15 | payload: { modal: null }, 16 | }); 17 | } 18 | -------------------------------------------------------------------------------- /src/controllers/frontend/emulate.ts: -------------------------------------------------------------------------------- 1 | import { Router } from 'express'; 2 | 3 | import { SocketIOApp } from '../../lib/utils/ws.js'; 4 | 5 | export default function emulateController(router: Router, ws: SocketIOApp) { 6 | router.route('/command').post(async (req, res: any) => { 7 | const socketRes = await ws.emulate(req.body.cmd, req.body.body, req.headers); 8 | if (!socketRes.err) { 9 | res.status(200).json(socketRes); 10 | } else { 11 | res.status(socketRes?.data?.code || 500).json(socketRes); 12 | } 13 | }); 14 | } 15 | -------------------------------------------------------------------------------- /src/dao/stats.ts: -------------------------------------------------------------------------------- 1 | import { db } from '../lib/dao/database.js'; 2 | import { DBStatsPlayed, DBStatsRequested } from '../types/database/stats.js'; 3 | import { sqlexportPlayed, sqlexportRequested } from './sql/stats.js'; 4 | 5 | export async function selectPlayed(): Promise { 6 | const res = await db().query(sqlexportPlayed); 7 | return res.rows; 8 | } 9 | 10 | export async function selectRequests(): Promise { 11 | const res = await db().query(sqlexportRequested); 12 | return res.rows; 13 | } 14 | -------------------------------------------------------------------------------- /src/types/database/database.d.ts: -------------------------------------------------------------------------------- 1 | export interface DBStats { 2 | singers: number; 3 | songwriters: number; 4 | creators: number; 5 | authors: number; 6 | karas: number; 7 | languages: number; 8 | usagetime: number; 9 | playtime: number; 10 | series: number; 11 | played: number; 12 | playlists: number; 13 | duration: number; 14 | blacklist: number; 15 | whitelist: number; 16 | tags: number; 17 | total_media_size: number; 18 | } 19 | 20 | export interface DBSetting { 21 | option: string; 22 | value: string; 23 | } 24 | -------------------------------------------------------------------------------- /kmfrontend/src/frontend/components/modals/AdminMessageModal.scss: -------------------------------------------------------------------------------- 1 | .admin-message { 2 | .dest-duration { 3 | display: flex; 4 | justify-content: space-evenly; 5 | margin-bottom: 0.5em; 6 | 7 | input[type='number'].duration { 8 | background-color: #343434; 9 | color: white; 10 | border-radius: 0.5em; 11 | border: 1px solid #fafafa70; 12 | padding: 0 0.5em; 13 | &:focus { 14 | box-shadow: 0 1px 0 0 #546e7a; 15 | } 16 | } 17 | } 18 | 19 | input[type='text'].message { 20 | height: 2em; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /kmfrontend/src/toast.scss: -------------------------------------------------------------------------------- 1 | .toast-with-img { 2 | display: flex; 3 | align-items: center; 4 | width: 100%; 5 | height: auto; 6 | > picture { 7 | flex: 1 0 30%; 8 | padding: 0.125em 0.2em; 9 | > img { 10 | height: auto; 11 | max-width: 100%; 12 | } 13 | } 14 | > span { 15 | text-align: start; 16 | } 17 | } 18 | 19 | :root { 20 | // React Toastify 21 | --toastify-color-info: #1861b1; 22 | --toastify-color-success: #099242; 23 | --toastify-color-warning: #ae5b13; 24 | --toastify-color-error: #7d140b; 25 | } 26 | -------------------------------------------------------------------------------- /kmfrontend/vite.config.mts: -------------------------------------------------------------------------------- 1 | import legacy from '@vitejs/plugin-legacy'; 2 | import react from '@vitejs/plugin-react'; 3 | import { defineConfig } from 'vite'; 4 | import { nodePolyfills } from 'vite-plugin-node-polyfills'; 5 | 6 | export default defineConfig({ 7 | build: { 8 | sourcemap: true, 9 | }, 10 | plugins: [nodePolyfills(), react(), legacy()], 11 | server: { 12 | port: 3000, 13 | proxy: { 14 | '/avatars': 'http://localhost:1337', 15 | '/previews': 'http://localhost:1337', 16 | '/api': 'http://localhost:1337', 17 | }, 18 | }, 19 | }); 20 | -------------------------------------------------------------------------------- /src/types/poll.d.ts: -------------------------------------------------------------------------------- 1 | import { DBPLC } from '../lib/types/database/playlist.js'; 2 | 3 | export interface PollState { 4 | songPoll: boolean; 5 | } 6 | 7 | export interface PollResults { 8 | votes: number; 9 | winner: PollItem; 10 | index: number; 11 | } 12 | 13 | export interface PollItem extends DBPLC { 14 | votes?: number; 15 | index?: number; 16 | } 17 | 18 | export interface PollObject { 19 | infos: { count: number; from: number; to: number }; 20 | poll: PollItem[]; 21 | timeLeft: number; 22 | flag_uservoted: boolean; 23 | } 24 | -------------------------------------------------------------------------------- /src/dao/sql/upvote.ts: -------------------------------------------------------------------------------- 1 | // SQL for favorites management 2 | 3 | export const sqlinsertUpvote = ` 4 | INSERT INTO upvote( 5 | fk_plcid, 6 | fk_login 7 | ) 8 | VALUES( 9 | :plc_id, 10 | :username 11 | ); 12 | `; 13 | 14 | export const sqldeleteUpvote = ` 15 | DELETE FROM upvote 16 | WHERE fk_plcid = :plc_id 17 | AND fk_login = :username 18 | `; 19 | 20 | export const sqlselectUpvoteByPLC = ` 21 | SELECT u.fk_login AS username, us.nickname 22 | FROM upvote u 23 | LEFT JOIN users us ON us.pk_login = u.fk_login 24 | WHERE u.fk_plcid = $1; 25 | `; 26 | -------------------------------------------------------------------------------- /util/cleanupReleases.sh: -------------------------------------------------------------------------------- 1 | source util/versionUtil.sh 2 | 3 | lftp -u $USERNAME,$PASSWORD $KARAOKESMOE <<__CMD__ 4 | set cmd:fail-exit yes 5 | cls www/mugen.karaokes.moe/downloads/ | grep -- "-$RELEASE" | grep -v -- "$BUILDVERSION" | sed -e 's/^/\"/g' | sed -e 's/$/\"/g' | xargs -0 -I{} echo "{}" | sed 's/^/rm\ /g' >> rm_list.txt 6 | __CMD__ 7 | 8 | # Remove last line 9 | sed -i '$ d' rm_list.txt 10 | 11 | cat rm_list.txt 12 | 13 | lftp -u $USERNAME,$PASSWORD $KARAOKESMOE <<__CMD__ 14 | set cmd:fail-exit yes 15 | source rm_list.txt 16 | __CMD__ 17 | 18 | rm rm_list.txt -------------------------------------------------------------------------------- /kmfrontend/src/systempanel/pages/Karas/KaraListPage.tsx: -------------------------------------------------------------------------------- 1 | import { Layout } from 'antd'; 2 | import i18next from 'i18next'; 3 | 4 | import KaraList from '../../components/KaraList'; 5 | import Title from '../../components/Title'; 6 | 7 | function KaraListPage() { 8 | return ( 9 | <> 10 | 14 | <Layout.Content> 15 | <KaraList /> 16 | </Layout.Content> 17 | </> 18 | ); 19 | } 20 | 21 | export default KaraListPage; 22 | -------------------------------------------------------------------------------- /kmfrontend/src/frontend/components/modals/PlaylistModal.scss: -------------------------------------------------------------------------------- 1 | #playlistModal { 2 | .error { 3 | font-weight: bold; 4 | font-size: large; 5 | color: orangered; 6 | margin: 0.5em; 7 | } 8 | 9 | .modal-body.flex-direction-btns > div { 10 | width: 95%; 11 | 12 | button { 13 | > div { 14 | width: 50em; 15 | align-items: flex-start; 16 | } 17 | 18 | .title { 19 | font-size: 1.2em; 20 | 21 | i { 22 | margin-right: 0.3em; 23 | } 24 | } 25 | 26 | .desc { 27 | text-align: left; 28 | } 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Set the default behavior, in case people don't have core.autocrlf set. 2 | * text=auto 3 | 4 | # Explicitly declare text files you want to always be normalized and converted 5 | # to native line endings on checkout. 6 | *.js text eol=crlf 7 | *.json text eol=crlf 8 | *.ini text eol=crlf 9 | *.sql text eol=crlf 10 | *.ass text eol=crlf 11 | *.md text eol=crlf 12 | *.ts text eol=crlf 13 | *.tsx text eol=crlf 14 | *.scss text eol=crlf 15 | 16 | # Denote all files that are truly binary and should not be modified. 17 | *.png binary 18 | *.jpg binary 19 | *.gif binary 20 | *.webp binary 21 | *.icns binary -------------------------------------------------------------------------------- /kmfrontend/src/frontend/styles/components/details.scss: -------------------------------------------------------------------------------- 1 | @use 'sass:color'; 2 | @use '../variables'; 3 | 4 | details { 5 | margin: 0.5rem; 6 | background-color: color.scale(variables.$mugen-background, $lightness: 10%); 7 | border-radius: 8px; 8 | border: solid 1px color.scale(variables.$mugen-background, $lightness: 25%); 9 | 10 | summary { 11 | user-select: none; 12 | padding: 0.5rem; 13 | } 14 | &[open] summary { 15 | border-bottom: solid 1px color.scale(variables.$mugen-background, $lightness: 25%); 16 | } 17 | 18 | > :not(summary) { 19 | padding: 1em; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/dao/sql/stats.ts: -------------------------------------------------------------------------------- 1 | // SQL for stats 2 | 3 | export const sqlexportPlayed = ` 4 | SELECT p.fk_kid AS kid, 5 | p.fk_seid AS seid, 6 | p.played_at 7 | FROM played p, session s 8 | WHERE s.pk_seid = p.fk_seid 9 | AND s.private = FALSE; 10 | `; 11 | 12 | export const sqlexportRequested = ` 13 | SELECT r.fk_kid AS kid, 14 | r.fk_seid AS seid, 15 | r.requested_at, 16 | r.fk_login AS username 17 | FROM requested r 18 | LEFT JOIN session s ON s.pk_seid = r.fk_seid 19 | LEFT JOIN users u ON u.pk_login = r.fk_login 20 | WHERE s.private = FALSE 21 | AND u.flag_sendstats = TRUE; 22 | `; 23 | -------------------------------------------------------------------------------- /src/controllers/frontend/test.ts: -------------------------------------------------------------------------------- 1 | // These routes are only available in --test mode 2 | 3 | import { WS_CMD } from '../../../kmfrontend/src/utils/ws.js'; 4 | import { getConfig } from '../../lib/utils/config.js'; 5 | import { SocketIOApp } from '../../lib/utils/ws.js'; 6 | import { getState } from '../../utils/state.js'; 7 | 8 | export default function testController(router: SocketIOApp) { 9 | router.route(WS_CMD.GET_STATE, async (_socket, _req) => { 10 | return getState(); 11 | }); 12 | router.route(WS_CMD.GET_FULL_CONFIG, async (_socket, _req) => { 13 | return getConfig(); 14 | }); 15 | } 16 | -------------------------------------------------------------------------------- /kmfrontend/src/frontend/components/karas/ActionsButtons.scss: -------------------------------------------------------------------------------- 1 | @use 'sass:map'; 2 | @use '../../styles/variables'; 3 | 4 | @media (max-width: variables.$mugen-breakpoint-large) { 5 | .karaLineButton { 6 | min-height: 3em; 7 | font-size: 1.25em; 8 | 9 | &.showPlaylistCommands { 10 | width: 2.75em; 11 | } 12 | } 13 | } 14 | 15 | .karaLineButton.yellow { 16 | color: map.get(variables.$mugen-colors, 'yellow'); 17 | } 18 | 19 | .currentUpvote { 20 | color: orangered; 21 | } 22 | 23 | button.on { 24 | background: #3c5c00; 25 | } 26 | button.off { 27 | background: #880500; 28 | } 29 | -------------------------------------------------------------------------------- /src/utils/displays.ts: -------------------------------------------------------------------------------- 1 | import { graphics } from 'systeminformation'; 2 | 3 | import logger from '../lib/utils/logger.js'; 4 | 5 | const service = 'Displays'; 6 | 7 | /** Get list of displays connected to the computer */ 8 | export async function getDisplays() { 9 | // Get list of monitors to allow users to select one for the player 10 | const data = await graphics(); 11 | logger.debug('Displays detected', { service, obj: data }); 12 | return data.displays 13 | .filter(d => d.resolutionX > 0) 14 | .map(d => { 15 | d.model = d.model.replaceAll('�', 'e'); 16 | return d; 17 | }); 18 | } 19 | -------------------------------------------------------------------------------- /test/poll.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | 3 | import { commandBackend, getToken } from './util/util.js'; 4 | 5 | describe('Song Poll', () => { 6 | let token: string; 7 | before(async () => { 8 | token = await getToken(); 9 | }); 10 | it('Get current poll status', async () => { 11 | const data = await commandBackend(token, 'getPoll', undefined, true); 12 | expect(data.code).to.be.equal(425); 13 | }); 14 | 15 | it('Set poll', async () => { 16 | const data = await commandBackend(token, 'votePoll', { index: 1 }, true); 17 | expect(data.message.code).to.be.equal('POLL_NOT_ACTIVE'); 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /util/updateFlatpak.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | SENTRYCLI_VERSION=$(grep @sentry/cli\": package.json | awk -F\" {'print $4'} | sed -e 's/\^//g') 4 | 5 | wget --quiet -N https://downloads.sentry-cdn.com/sentry-cli/$SENTRYCLI_VERSION/sentry-cli-Linux-x86_64 6 | 7 | wget --quiet -N https://downloads.sentry-cdn.com/sentry-cli/$SENTRYCLI_VERSION/sentry-cli-Linux-aarch64 8 | 9 | SENTRYCLI_X64_SHA=$(sha256sum sentry-cli-Linux-x86_64 | awk -F\ {'print $1'}) 10 | 11 | SENTRYCLI_ARM64_SHA=$(sha256sum sentry-cli-Linux-aarch64 | awk -F\ {'print $1'}) 12 | 13 | node util/updateFlatpak.cjs "$SENTRYCLI_VERSION" "$SENTRYCLI_X64_SHA" "$SENTRYCLI_ARM64_SHA" 14 | -------------------------------------------------------------------------------- /kmfrontend/src/systempanel/components/Title.tsx: -------------------------------------------------------------------------------- 1 | import { Layout } from 'antd'; 2 | 3 | import TasksEvent from '../../TasksEvent'; 4 | 5 | interface Props { 6 | title: string; 7 | description: string; 8 | } 9 | 10 | export default function Title(props: Props) { 11 | return ( 12 | <Layout.Header> 13 | <div style={{ display: 'flex', justifyContent: 'space-between' }}> 14 | <div> 15 | <div className="title">{props.title}</div> 16 | <div className="description">{props.description}</div> 17 | </div> 18 | <TasksEvent limit={3} styleTask="system-tasks-wrapper" /> 19 | </div> 20 | </Layout.Header> 21 | ); 22 | } 23 | -------------------------------------------------------------------------------- /migrations/20211217182244.do.newUserFields.sql: -------------------------------------------------------------------------------- 1 | alter table users add column IF NOT EXISTS flag_public boolean default(true) not null; 2 | alter table users add column IF NOT EXISTS flag_displayfavorites boolean default(false) not null; 3 | alter table users add column IF NOT EXISTS social_networks jsonb default('{"discord":"", "twitter": "", "twitch": "", "instagram": ""}') not null; 4 | alter table users add column IF NOT EXISTS banner character varying default('default.jpg') not null; 5 | 6 | update users set flag_public = true, flag_displayfavorites = false, social_networks = '{"discord":"", "twitter": "", "twitch": "", "instagram": ""}', banner = 'default.jpg'; 7 | -------------------------------------------------------------------------------- /util/i18next-scanner.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | input: ['src/**/*.{ts,tsx}'], 3 | output: './', 4 | options: { 5 | debug: true, 6 | removeUnusedKeys: false, 7 | func: { 8 | list: ['i18next.t', 'i18n.t'], 9 | extensions: ['.ts', '.tsx'], 10 | }, 11 | lngs: ['en', 'fr', 'es', 'id', 'pt', 'de', 'it', 'pl', 'ta', 'br', 'ru'], 12 | defaultLng: 'en', 13 | defaultValue: '__STRING_NOT_TRANSLATED__', 14 | resource: { 15 | loadPath: 'locales/{{lng}}.json', 16 | savePath: 'locales/{{lng}}.json', 17 | jsonIndent: 2, 18 | lineEnding: '\n', 19 | }, 20 | interpolation: { 21 | prefix: '{{', 22 | suffix: '}}', 23 | }, 24 | }, 25 | }; 26 | -------------------------------------------------------------------------------- /util/socketClient.js: -------------------------------------------------------------------------------- 1 | /** 2 | * How to use 3 | * 4 | * ts-node socketClient.js 5 | * in a TTY-compatible terminal (cmd on Windows, not git bash) 6 | * use sendCommand('login', {body: {...}}) with the right params 7 | * 8 | */ 9 | 10 | import repl from 'repl'; 11 | import { io } from 'socket.io-client'; 12 | 13 | const socket = io(process.argv[2] || 'http://localhost:1337'); 14 | 15 | function sendCommand(name, data) { 16 | socket.emit(name, data, ack => { 17 | console.log(JSON.stringify(ack, null, 2)); 18 | }); 19 | } 20 | 21 | const REPLServer = repl.start(); 22 | REPLServer.context.sendCommand = sendCommand; 23 | REPLServer.displayPrompt(); 24 | -------------------------------------------------------------------------------- /kmfrontend/src/store/context.ts: -------------------------------------------------------------------------------- 1 | import { createContext, Dispatch } from 'react'; 2 | 3 | import { AuthStore } from './types/auth'; 4 | import { FrontendContextStore } from './types/frontendContext'; 5 | import { ModalStore } from './types/modal'; 6 | import { SettingsStore } from './types/settings'; 7 | 8 | export interface GlobalContextInterface { 9 | globalState: { 10 | auth: AuthStore; 11 | frontendContext: FrontendContextStore; 12 | settings: SettingsStore; 13 | modal: ModalStore; 14 | }; 15 | globalDispatch: Dispatch<any>; 16 | } 17 | 18 | const GlobalContext = createContext<GlobalContextInterface>(null); 19 | 20 | export default GlobalContext; 21 | -------------------------------------------------------------------------------- /src/dao/sql/favorites.ts: -------------------------------------------------------------------------------- 1 | // SQL for favorites management 2 | 3 | export const sqlgetFavoritesMicro = (limitClause: string, offsetClause: string) => ` 4 | SELECT 5 | fk_kid AS kid 6 | FROM favorites 7 | WHERE fk_login = :username 8 | ${limitClause} 9 | ${offsetClause} 10 | `; 11 | 12 | export const sqlremoveFavorites = ` 13 | DELETE FROM favorites 14 | WHERE fk_kid = $1 15 | AND fk_login = $2; 16 | `; 17 | 18 | export const sqlclearFavorites = ` 19 | DELETE FROM favorites 20 | WHERE fk_login = $1; 21 | `; 22 | 23 | export const sqlinsertFavorites = ` 24 | INSERT INTO favorites(fk_kid, fk_login) 25 | VALUES ($1, $2) ON CONFLICT DO NOTHING 26 | `; 27 | -------------------------------------------------------------------------------- /kmfrontend/src/frontend/components/migrations/Migration.tsx: -------------------------------------------------------------------------------- 1 | import i18next from 'i18next'; 2 | 3 | import { commandBackend } from '../../../utils/socket'; 4 | import { WS_CMD } from '../../../utils/ws'; 5 | 6 | export default function useMigration(name: string, onEnd: () => void): [() => JSX.Element, () => void] { 7 | const EndButton = () => ( 8 | <button className="continue-btn" onClick={saveMigration}> 9 | {i18next.t('MIGRATE.CONTINUE')} 10 | </button> 11 | ); 12 | 13 | function saveMigration() { 14 | commandBackend(WS_CMD.SET_MIGRATIONS_FRONTEND, { mig: { name, flag_done: true } }).then(onEnd); 15 | } 16 | 17 | return [EndButton, saveMigration]; 18 | } 19 | -------------------------------------------------------------------------------- /kmfrontend/src/frontend/components/generic/buttons/ShowVideoButton.tsx: -------------------------------------------------------------------------------- 1 | import i18next from 'i18next'; 2 | 3 | import { isRemote } from '../../../../utils/socket'; 4 | 5 | interface Props { 6 | togglePreview: () => void; 7 | preview: boolean; 8 | repository: string; 9 | } 10 | 11 | export default function ShowVideoButton(props: Props) { 12 | return isRemote() && !/\./.test(props.repository) ? null : ( 13 | <button type="button" className="btn btn-action" onClick={props.togglePreview}> 14 | <i className="fas fa-video" /> 15 | <span>{props.preview ? i18next.t('KARA_DETAIL.HIDE_VIDEO') : i18next.t('KARA_DETAIL.SHOW_VIDEO')}</span> 16 | </button> 17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /kmfrontend/src/index.tsx: -------------------------------------------------------------------------------- 1 | import './common.scss'; 2 | import './utils/electron'; 3 | import './utils/i18n'; 4 | import './utils/isoLanguages'; 5 | import './utils/polyfills'; 6 | import './utils/socket'; 7 | 8 | import { createRoot } from 'react-dom/client'; 9 | import { BrowserRouter } from 'react-router-dom'; 10 | 11 | import App from './App'; 12 | import GlobalStateProvider from './store/GlobalStateProvider'; 13 | 14 | const container = document.getElementById('mountpoint'); 15 | const root = createRoot(container); 16 | 17 | root.render( 18 | <GlobalStateProvider> 19 | <BrowserRouter> 20 | <App /> 21 | </BrowserRouter> 22 | </GlobalStateProvider> 23 | ); 24 | -------------------------------------------------------------------------------- /kmfrontend/src/frontend/components/karas/InlineTag.scss: -------------------------------------------------------------------------------- 1 | .inline-tag { 2 | display: inline-block; 3 | position: relative; 4 | } 5 | 6 | .inline-tag.public span { 7 | cursor: zoom-in; 8 | } 9 | 10 | .tag-popup { 11 | position: absolute; 12 | background-color: black; 13 | margin-top: 0.25em; 14 | padding: 0.5em; 15 | width: max-content; 16 | z-index: 2; 17 | color: white; 18 | &.right { 19 | right: 0; 20 | } 21 | 22 | .tag-name { 23 | font-weight: bold; 24 | font-size: 1.1em; 25 | } 26 | .tag-stat { 27 | font-weight: lighter; 28 | font-style: italic; 29 | } 30 | .tag-action { 31 | font-weight: bold; 32 | margin-top: 0.25em; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /config.CICD.yml: -------------------------------------------------------------------------------- 1 | Karaoke: 2 | Collections: 3 | c7db86a0-ff64-4044-9be4-66dd1ef1d1c1: true # Geek 4 | dbcf2c22-524d-4708-99bb-601703633927: true # Asia 5 | efe171c0-e8a1-4d03-98c0-60ecf741ad52: true # West 6 | 2fa2fe3f-bb56-45ee-aa38-eae60e76f224: true # Shitpost 7 | System: 8 | Database: 9 | bundledPostgresBinary: false 10 | database: karaokemugen_app 11 | host: postgres 12 | password: musubi 13 | port: 5432 14 | superuser: karaokemugen_app 15 | superuserPassword: musubi 16 | username: karaokemugen_app 17 | connection: tcp 18 | Binaries: 19 | Player: 20 | Linux: mpv 21 | ffmpeg: 22 | Linux: ffmpeg 23 | patch: 24 | Linux: patch 25 | -------------------------------------------------------------------------------- /kmfrontend/src/utils/i18next-scanner.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | input: ['src/**/*.{ts,tsx}'], 3 | output: './', 4 | options: { 5 | debug: true, 6 | removeUnusedKeys: false, 7 | func: { 8 | list: ['i18next.t', 'i18n.t'], 9 | extensions: ['.ts', '.tsx'], 10 | }, 11 | lngs: ['en', 'fr', 'es', 'id', 'pt', 'de', 'it', 'pl', 'ta', 'br', 'ru'], 12 | defaultLng: 'en', 13 | defaultValue: '__STRING_NOT_TRANSLATED__', 14 | resource: { 15 | loadPath: 'src/locales/{{lng}}.json', 16 | savePath: 'src/locales/{{lng}}.json', 17 | jsonIndent: 2, 18 | lineEnding: '\n', 19 | }, 20 | interpolation: { 21 | prefix: '{{', 22 | suffix: '}}', 23 | }, 24 | }, 25 | }; 26 | -------------------------------------------------------------------------------- /migrations/20240123000000.do.migratingGuestNames.sql: -------------------------------------------------------------------------------- 1 | UPDATE users SET pk_login = lower(unaccent(pk_login)) 2 | WHERE type = 2; 3 | 4 | UPDATE users SET pk_login = replace(pk_login, ' ', '_') 5 | WHERE type = 2; 6 | 7 | UPDATE users SET pk_login = replace(pk_login, '''', '') 8 | WHERE type = 2; 9 | 10 | UPDATE users SET pk_login = replace(pk_login, '!', '') 11 | WHERE type = 2; 12 | 13 | UPDATE users SET pk_login = replace(pk_login, '/', '_') 14 | WHERE type = 2; 15 | 16 | UPDATE users SET pk_login = replace(pk_login, '<', '') 17 | WHERE type = 2; 18 | 19 | UPDATE users SET pk_login = replace(pk_login, '?', '_') 20 | WHERE type = 2; 21 | 22 | UPDATE users SET pk_login = replace(pk_login, '"', '') 23 | WHERE type = 2; -------------------------------------------------------------------------------- /kmfrontend/src/frontend/styles/components/loader.scss: -------------------------------------------------------------------------------- 1 | .loader { 2 | border: 16px solid #9e9e9e; 3 | border-radius: 50%; 4 | border-top: 16px solid #3498db; 5 | left: calc(50% - 34px * 3 / 2); 6 | top: calc(50% - 34px * 3 / 2); 7 | position: absolute; 8 | width: calc(34px * 3); 9 | height: calc(34px * 3); 10 | animation: spin 0.8s linear infinite; 11 | } 12 | 13 | .inline-loader { 14 | border: 3px solid #9e9e9e; 15 | border-radius: 50%; 16 | border-top: 3px solid #3498db; 17 | aspect-ratio: 1 / 1; 18 | animation: spin 0.8s linear infinite; 19 | width: 1em; 20 | } 21 | 22 | @keyframes spin { 23 | 0% { 24 | transform: rotate(0deg); 25 | } 26 | 100% { 27 | transform: rotate(360deg); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/dao/migrationsFrontend.ts: -------------------------------------------------------------------------------- 1 | import { db } from '../lib/dao/database.js'; 2 | import { MigrationsFrontend } from '../types/database/migrationsFrontend.js'; 3 | import { sqlMarkAllMigrationsAsDone, sqlSelectAllMigrations, sqlUpdateMigrations } from './sql/migrationsFrontend.js'; 4 | 5 | export async function selectMigrationsFrontend(): Promise<MigrationsFrontend[]> { 6 | const migs = await db().query(sqlSelectAllMigrations); 7 | return migs.rows; 8 | } 9 | 10 | export async function updateMigrationsFrontend(mig: MigrationsFrontend) { 11 | await db().query(sqlUpdateMigrations, [mig.name, mig.flag_done]); 12 | } 13 | 14 | export async function markAllMigrationsFrontendAsDone() { 15 | await db().query(sqlMarkAllMigrationsAsDone); 16 | } 17 | -------------------------------------------------------------------------------- /test/player.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | 3 | import { uuidRegexp } from '../src/lib/utils/constants.js'; 4 | import { commandBackend, getToken } from './util/util.js'; 5 | 6 | describe('Player', () => { 7 | let token: string; 8 | before(async () => { 9 | token = await getToken(); 10 | }); 11 | it('Get player status', async () => { 12 | const data = await commandBackend(token, 'getPlayerStatus'); 13 | expect(data.currentRequester).to.satisfy((e: any) => typeof e === 'string' || e === null); 14 | expect(data.currentSessionID).to.be.a('string').and.match(uuidRegexp); 15 | expect(data.defaultLocale).to.be.a('string').and.have.lengthOf(2); 16 | // other data cannot be tested because of mpv lack in CI 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /kmfrontend/src/utils/PrivateRoute.tsx: -------------------------------------------------------------------------------- 1 | import { useContext } from 'react'; 2 | import { Navigate, Route, Routes, useLocation } from 'react-router-dom'; 3 | 4 | import GlobalContext from '../store/context'; 5 | import { setLastLocation } from './tools'; 6 | 7 | interface Props { 8 | component: any; 9 | } 10 | 11 | function PrivateRoute(props: Props) { 12 | const context = useContext(GlobalContext); 13 | const location = useLocation(); 14 | 15 | setLastLocation(location.pathname); 16 | return context.globalState.auth.isAuthenticated ? ( 17 | props.component 18 | ) : ( 19 | <Routes> 20 | <Route path="*" element={<Navigate to={`/login${location.search}`} />} /> 21 | </Routes> 22 | ); 23 | } 24 | 25 | export default PrivateRoute; 26 | -------------------------------------------------------------------------------- /.gitlab/issue_templates/Bug.md: -------------------------------------------------------------------------------- 1 | # Summary 2 | 3 | (Describe the bug you encounter in an easy way) 4 | 5 | # Which version and OS are you using? 6 | 7 | (Go to the About... screen from the Help menu and copy/paste version number and branch, commit SHA and so on) 8 | 9 | (For Linux users, please mention your distribution) 10 | 11 | # How to reproduce the bug? 12 | 13 | (If possible, tell us in simple steps how to reproduce this) 14 | 15 | # What's the actual bug behavior? 16 | 17 | (What happens right now when you do those steps) 18 | 19 | # What should happen instead? 20 | 21 | (What were you expecting instead of the bug?) 22 | 23 | # Logs and/or screenshots 24 | 25 | (Please add your karaokemugen log file here and/or relevant screenshots) 26 | -------------------------------------------------------------------------------- /src/utils/logger.ts: -------------------------------------------------------------------------------- 1 | import { shell } from 'electron'; 2 | import { resolve } from 'path'; 3 | 4 | import { resolvedPath } from '../lib/utils/config.js'; 5 | import { date } from '../lib/utils/date.js'; 6 | import { ErrorKM } from '../lib/utils/error.js'; 7 | import logger from '../lib/utils/logger.js'; 8 | import Sentry from './sentry.js'; 9 | 10 | const service = 'Logger'; 11 | 12 | export async function selectLogFile() { 13 | try { 14 | const fpath = resolve(resolvedPath('Logs'), `karaokemugen-${date()}.log`); 15 | shell.showItemInFolder(fpath); 16 | } catch (err) { 17 | logger.error(`Unable to open file explorer on log directory: ${err}`, { service }); 18 | Sentry.error(err); 19 | throw new ErrorKM('LOG_VIEW_ERROR'); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /kmfrontend/src/frontend/components/generic/RadioButton.scss: -------------------------------------------------------------------------------- 1 | @use '../../styles/variables.scss'; 2 | 3 | .radiobutton-ui { 4 | display: flex; 5 | flex-direction: row; 6 | &[data-orientation='vertical'] { 7 | flex-direction: column; 8 | } 9 | 10 | button { 11 | display: block; 12 | flex: 1; 13 | padding: 0; 14 | margin: 1px; 15 | border: none; 16 | background: variables.$mugen-radio-btn-background; 17 | color: variables.$mugen-radio-btn-color; 18 | outline: none; 19 | transition: 20 | color ease 0.5s, 21 | background ease 0.5s; 22 | cursor: pointer; 23 | &.active { 24 | background: variables.$mugen-radio-btn-default-active-background; 25 | color: variables.$mugen-radio-btn-default-active-color; 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /migrations/20200516000000.do.addBLCSets.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE blacklist_criteria_set ( 2 | pk_id_blc_set SERIAL PRIMARY KEY, 3 | name CHARACTER VARYING NOT NULL, 4 | created_at TIMESTAMPTZ NOT NULL, 5 | modified_at TIMESTAMPTZ NOT NULL, 6 | flag_current BOOLEAN DEFAULT FALSE 7 | ); 8 | 9 | INSERT INTO blacklist_criteria_set(name, created_at, modified_at, flag_current) VALUES('Blacklist 1', NOW(), NOW(), true); 10 | 11 | ALTER TABLE blacklist_criteria ADD COLUMN fk_id_blc_set INTEGER; 12 | UPDATE blacklist_criteria SET fk_id_blc_set = 1; 13 | ALTER TABLE blacklist_criteria ADD CONSTRAINT blc_id_blc_set_fk FOREIGN KEY (fk_id_blc_set) REFERENCES blacklist_criteria_set(pk_id_blc_set) ON DELETE CASCADE; 14 | ALTER TABLE blacklist_criteria ALTER COLUMN fk_id_blc_set SET NOT NULL; 15 | -------------------------------------------------------------------------------- /kmfrontend/src/frontend/components/About.scss: -------------------------------------------------------------------------------- 1 | @use 'sass:color'; 2 | 3 | .about-page { 4 | padding: 1em; 5 | display: flex; 6 | min-height: 100vh; 7 | flex-direction: column; 8 | align-items: center; 9 | justify-content: center; 10 | text-align: center; 11 | > p { 12 | margin: 0.5em 0; 13 | } 14 | .awesome-person { 15 | font-weight: bold; 16 | color: color.scale(#26aacc, $lightness: 40%); 17 | } 18 | .app-presentation { 19 | img { 20 | max-width: 250px; 21 | } 22 | } 23 | .technical-stuff { 24 | width: 100%; 25 | .version { 26 | display: flex; 27 | > div:first-child { 28 | width: 45%; 29 | text-align: right; 30 | } 31 | > div.separator { 32 | flex-grow: 0; 33 | width: 1em; 34 | } 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/types/stats.d.ts: -------------------------------------------------------------------------------- 1 | import { Config } from './config.js'; 2 | import { DBStatsPlayed, DBStatsRequested } from './database/stats.js'; 3 | 4 | export interface StatsPayload { 5 | payloadVersion: number; 6 | instance: Instance; 7 | viewcounts: DBStatsPlayed[]; 8 | requests: DBStatsRequested[]; 9 | favorites: Favorite[]; 10 | } 11 | 12 | interface Instance { 13 | config: Config; 14 | instance_id: string; 15 | version: number; 16 | locale: string; 17 | screens: number; 18 | cpu_manufacturer: string; 19 | cpu_model: string; 20 | cpu_speed: string; 21 | cpu_cores: number; 22 | memory: number; 23 | total_disk_space: number; 24 | os_platform: string; 25 | os_distro: string; 26 | os_release: string; 27 | } 28 | 29 | interface Favorite { 30 | kid: string; 31 | } 32 | -------------------------------------------------------------------------------- /kmfrontend/src/frontend/components/decorators/PlaylistMainDecorator.tsx: -------------------------------------------------------------------------------- 1 | import { ReactNodeArray } from 'react'; 2 | 3 | interface IProps { 4 | children: ReactNodeArray; 5 | } 6 | 7 | export default function PlaylistMainDecorator(props: IProps) { 8 | return ( 9 | <div className="PlaylistMainDecorator"> 10 | <div className="playlist-main"> 11 | {props.children.map ? ( 12 | props.children.map((node: any, index: number) => { 13 | const i = index + 1; 14 | return ( 15 | <div key={index} className="panel" id={'panel' + i}> 16 | {node} 17 | </div> 18 | ); 19 | }) 20 | ) : ( 21 | <div key={1} className="panel" id="panel1"> 22 | {props.children} 23 | </div> 24 | )} 25 | </div> 26 | </div> 27 | ); 28 | } 29 | -------------------------------------------------------------------------------- /src/types/playlist.d.ts: -------------------------------------------------------------------------------- 1 | import { DBKara } from '../lib/types/database/kara.js'; 2 | import { DBPLCBase } from '../lib/types/database/playlist.js'; 3 | import { AggregatedCriteria } from '../lib/types/playlist.js'; 4 | 5 | export interface CurrentSong extends DBPLCBase, DBKara { 6 | avatar?: string; 7 | infos?: string; 8 | } 9 | 10 | export interface Pos { 11 | index: number; 12 | plc_id_pos: number; 13 | } 14 | 15 | export type ShuffleMethods = 'normal' | 'smart' | 'balance' | 'upvotes'; 16 | 17 | export interface AddKaraParams { 18 | kids: string[]; 19 | requester: string; 20 | plaid?: string; 21 | pos?: number; 22 | ignoreQuota?: boolean; 23 | refresh?: boolean; 24 | criterias?: AggregatedCriteria[]; 25 | throwOnMissingKara?: boolean; 26 | visible?: boolean; 27 | } 28 | -------------------------------------------------------------------------------- /migrations/20211205000000.do.removeModifiedAtTags.sql: -------------------------------------------------------------------------------- 1 | DROP MATERIALIZED VIEW IF EXISTS all_tags; 2 | 3 | ALTER TABLE tag DROP COLUMN IF EXISTS modified_at; 4 | 5 | CREATE MATERIALIZED VIEW all_tags AS 6 | WITH t_count AS ( 7 | SELECT a.fk_tid, 8 | json_agg(json_build_object('type', a.type, 'count', a.c))::text AS count_per_type 9 | FROM (SELECT kara_tag.fk_tid, 10 | count(kara_tag.fk_kid) AS c, 11 | kara_tag.type 12 | FROM kara_tag 13 | GROUP BY kara_tag.fk_tid, kara_tag.type) a 14 | GROUP BY a.fk_tid 15 | ) 16 | 17 | select t.*, 18 | t_count.count_per_type::jsonb AS karacount 19 | from tag t 20 | LEFT JOIN t_count ON t.pk_tid = t_count.fk_tid; 21 | 22 | CREATE UNIQUE INDEX idx_at_tid 23 | on all_tags (pk_tid); 24 | -------------------------------------------------------------------------------- /kmfrontend/src/frontend/components/modals/ShutdownModal.tsx: -------------------------------------------------------------------------------- 1 | import './ShutdownModal.scss'; 2 | 3 | import i18next from 'i18next'; 4 | 5 | interface IProps { 6 | close: () => void; 7 | } 8 | 9 | function ShutdownModal(props: IProps) { 10 | return ( 11 | <div className="shutdown-popup"> 12 | <div className="noise-wrapper"> 13 | <div className="noise" /> 14 | </div> 15 | <div className="shutdown-popup-text"> 16 | {i18next.t('SHUTDOWN_POPUP')} 17 | <br /> 18 | {'·´¯`(>_<)´¯`·'} 19 | </div> 20 | <button 21 | title={i18next.t('TOOLTIP_CLOSEPARENT')} 22 | className="closeParent btn btn-action" 23 | onClick={props.close} 24 | > 25 | <i className="fas fa-times" /> 26 | </button> 27 | </div> 28 | ); 29 | } 30 | 31 | export default ShutdownModal; 32 | -------------------------------------------------------------------------------- /kmfrontend/src/systempanel/pages/Options.tsx: -------------------------------------------------------------------------------- 1 | import { Component } from 'react'; 2 | 3 | import Config from './Config'; 4 | 5 | const propertiesToDisplay = [ 6 | 'App.QuickStart', 7 | 'Online.RemoteUsers.Enabled', 8 | 'Online.RemoteUsers.DefaultHost', 9 | 'Online.Discord.DisplayActivity', 10 | 'Online.FetchPopularSongs', 11 | 'Online.ErrorTracking', 12 | 'Player.HardwareDecoding', 13 | 'Player.KeyboardMediaShortcuts', 14 | 'Online.Updates.Medias.Jingles', 15 | 'Online.Updates.Medias.Sponsors', 16 | 'Online.Updates.Medias.Intros', 17 | 'Online.Updates.Medias.Outros', 18 | 'Online.Updates.Medias.Encores', 19 | ]; 20 | 21 | class Options extends Component<unknown, unknown> { 22 | render() { 23 | return <Config properties={propertiesToDisplay} />; 24 | } 25 | } 26 | 27 | export default Options; 28 | -------------------------------------------------------------------------------- /util/versionUtil.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Detects if we're running on a master or next release or tag release. 3 | # if not master or next, it's tag 4 | 5 | if [ "$CI_COMMIT_REF_NAME" = "master" ] || [ "$CI_COMMIT_REF_NAME" = "next" ] 6 | then 7 | export BUILDVERSION=$(grep version\": package.json | awk -F\" {'print $4'}) 8 | export RELEASE=$CI_COMMIT_REF_NAME 9 | else 10 | export BUILDVERSION=$(echo "$CI_COMMIT_REF_NAME" | awk -F- {'print $1'} | sed 's/v//g') 11 | if [ "$BUILDVERSION" = "" ] 12 | then 13 | export BUILDVERSION=$(grep version\": package.json | awk -F\" {'print $4'}) 14 | fi 15 | export RELEASE="release" 16 | fi 17 | 18 | export VERSION_NAME=$(grep versionName\": package.json | awk -F\" {'print $4'}) 19 | echo "Channel : $RELEASE" 20 | echo "Version number: $BUILDVERSION" 21 | echo "Version name : $VERSION_NAME" 22 | -------------------------------------------------------------------------------- /src/dao/upvote.ts: -------------------------------------------------------------------------------- 1 | import { pg as yesql } from 'yesql'; 2 | 3 | import { db } from '../lib/dao/database.js'; 4 | import { DBUpvote } from '../types/database/upvote.js'; 5 | import { sqldeleteUpvote, sqlinsertUpvote, sqlselectUpvoteByPLC } from './sql/upvote.js'; 6 | 7 | export async function selectUpvotesByPLC(plc_id: number): Promise<DBUpvote[]> { 8 | const res = await db().query(sqlselectUpvoteByPLC, [plc_id]); 9 | return res.rows; 10 | } 11 | 12 | export function insertUpvote(plc_id: number, username: string) { 13 | return db().query( 14 | yesql(sqlinsertUpvote)({ 15 | plc_id, 16 | username, 17 | }) 18 | ); 19 | } 20 | 21 | export function deleteUpvote(plc_id: number, username: string) { 22 | return db().query( 23 | yesql(sqldeleteUpvote)({ 24 | plc_id, 25 | username, 26 | }) 27 | ); 28 | } 29 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .idea/ 3 | .npmrc 4 | .vscode/settings.json 5 | .vs 6 | node_modules 7 | app/ 8 | app 9 | 10 | npm-debug.log* 11 | yarn-debug.log* 12 | yarn-error.log* 13 | *.exe 14 | karaokemugen*.log 15 | out/ 16 | doc/ 17 | \.DS_Store 18 | *.backup 19 | config\.yml 20 | *.bak 21 | karaokemugen-app-linux 22 | karaokemugen-app-macos 23 | dist/ 24 | bin/ 25 | logs/ 26 | updater/ 27 | mpv.log 28 | karaokemugen.pgdump 29 | karaokemugen.sql 30 | karaokemugen.sql.gz 31 | 32 | # kmfrontend 33 | kmfrontend/node_modules 34 | kmfrontend/coverage 35 | 36 | # sessions export 37 | *.played.csv 38 | *.playedCount.csv 39 | *.requested.csv 40 | *.requestedCount.csv 41 | dist2 42 | js 43 | packages 44 | postgrator.json 45 | 46 | # Dev 47 | .env 48 | .eslintcache 49 | 50 | # Prettier ignores 51 | package.json 52 | 53 | kmfrontend/src/locales 54 | locales/ -------------------------------------------------------------------------------- /migrations/20211016000000.do.addPlaylistSmartLimits.sql: -------------------------------------------------------------------------------- 1 | DO $$ 2 | BEGIN 3 | IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'playlist_smart_order') THEN 4 | CREATE TYPE playlist_smart_order AS 5 | ENUM('oldest', 'newest'); 6 | END IF; 7 | IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'playlist_smart_limit_type') THEN 8 | CREATE TYPE playlist_smart_limit_type AS 9 | ENUM('songs', 'duration'); 10 | END IF; 11 | END$$; 12 | ALTER TABLE playlist ADD COLUMN IF NOT EXISTS flag_smartlimit BOOLEAN DEFAULT(false); 13 | ALTER TABLE playlist ADD COLUMN IF NOT EXISTS smart_limit_number INTEGER DEFAULT(0); 14 | ALTER TABLE playlist ADD COLUMN IF NOT EXISTS smart_limit_order playlist_smart_order DEFAULT('newest'); 15 | ALTER TABLE playlist ADD COLUMN IF NOT EXISTS smart_limit_type playlist_smart_limit_type DEFAULT('songs'); -------------------------------------------------------------------------------- /kmfrontend/src/frontend/components/NotfoundPage.tsx: -------------------------------------------------------------------------------- 1 | import './NotfoundPage.scss'; 2 | 3 | import i18next from 'i18next'; 4 | import { Link, useLocation } from 'react-router-dom'; 5 | 6 | import image404 from '../../assets/nanami-surpris.png'; 7 | 8 | function NotfoundPage() { 9 | const location = useLocation(); 10 | 11 | return ( 12 | <div className="page404-lost"> 13 | <h1>{i18next.t('NOT_FOUND_PAGE.404')}</h1> 14 | <h3>{i18next.t('NOT_FOUND_PAGE.404_2')}</h3> 15 | <div className="you-are-here"> 16 | {location.pathname} <----- {i18next.t('NOT_FOUND_PAGE.404_3')} 17 | </div> 18 | <Link to="/" className="page404-btn"> 19 | {i18next.t('NOT_FOUND_PAGE.404_4')} 20 | </Link> 21 | <div> 22 | <img alt="" height="150" src={image404} /> 23 | </div> 24 | </div> 25 | ); 26 | } 27 | 28 | export default NotfoundPage; 29 | -------------------------------------------------------------------------------- /src/types/favorites.d.ts: -------------------------------------------------------------------------------- 1 | import { TagAndType } from '../lib/types/tag.js'; 2 | 3 | interface FavExportContent { 4 | kid: string; 5 | } 6 | 7 | export interface FavExport { 8 | Header: { 9 | description: string; 10 | version: number; 11 | }; 12 | Favorites: FavExportContent[]; 13 | } 14 | 15 | export interface AutoMixPlaylistInfo { 16 | plaid: string; 17 | playlist_name: string; 18 | } 19 | 20 | export type PlaylistLimit = 'duration' | 'songs'; 21 | 22 | export interface FavoritesMicro { 23 | kid: string; 24 | } 25 | 26 | export interface AutoMixParams { 27 | filters?: { 28 | usersFavorites?: string[]; 29 | usersAnimeList?: string[]; 30 | years?: number[]; 31 | tags?: TagAndType[]; 32 | }; 33 | limitType?: PlaylistLimit; 34 | limitNumber?: number; 35 | playlistName?: string; 36 | surprisePlaylist?: boolean; 37 | } 38 | -------------------------------------------------------------------------------- /assets/input.conf: -------------------------------------------------------------------------------- 1 | AXIS_UP add volume 2 2 | AXIS_DOWN add volume -2 3 | WHEEL_UP add volume 2 4 | WHEEL_DOWN add volume -2 5 | u ignore 6 | > script-message skip 7 | ENTER script-message skip 8 | MBTN_FORWARD script-message skip 9 | < script-message go-back 10 | MBTN_BACK script-message go-back 11 | RIGHT script-message seek 5 12 | LEFT script-message seek -5 13 | UP script-message seek 60 14 | DOWN script-message seek -60 15 | FORWARD script-message seek 60 16 | REWIND script-message seek -60 17 | Shift+RIGHT script-message seek 1 18 | Shift+LEFT script-message seek -1 19 | Shift+UP script-message seek 5 20 | Shift+DOWN script-message seek -5 21 | Shift+PGUP script-message seek 600 22 | Shift+PGDWN script-message seek -600 23 | MBTN_RIGHT script-message pause 24 | p script-message pause 25 | SPACE script-message pause 26 | v script-message subs 27 | -------------------------------------------------------------------------------- /kmfrontend/src/frontend/components/NotfoundPage.scss: -------------------------------------------------------------------------------- 1 | .page404-lost { 2 | padding: 1em 1em 0 1em; 3 | text-align: center; 4 | > h1 { 5 | font-size: 2em; 6 | font-weight: bold; 7 | } 8 | > h3 { 9 | font-size: 1.25em; 10 | font-weight: bold; 11 | } 12 | img { 13 | display: block; 14 | width: auto; 15 | height: 150px; 16 | margin: auto; 17 | } 18 | } 19 | 20 | .you-are-here { 21 | background-color: #404346; 22 | padding: 0.5em; 23 | width: max-content; 24 | margin: 1em auto; 25 | font-family: monospace; 26 | font-weight: bold; 27 | text-align: inherit; 28 | } 29 | 30 | .page404-btn:visited { 31 | color: white; 32 | } 33 | 34 | .page404-btn { 35 | display: block; 36 | background-color: #e2754a; 37 | width: fit-content; 38 | padding: 1em; 39 | margin: 1em auto; 40 | color: white; 41 | text-decoration: none; 42 | } 43 | -------------------------------------------------------------------------------- /src/electron/menus/view.ts: -------------------------------------------------------------------------------- 1 | import i18next from 'i18next'; 2 | 3 | import { MenuItemBuilderFunction } from '../../types/electron.js'; 4 | 5 | const builder: MenuItemBuilderFunction = () => { 6 | return { 7 | label: i18next.t('MENU_VIEW'), 8 | submenu: [ 9 | { label: i18next.t('MENU_VIEW_RELOAD'), role: 'reload' }, 10 | { label: i18next.t('MENU_VIEW_RELOADFORCE'), role: 'forceReload' }, 11 | { label: i18next.t('MENU_VIEW_TOGGLEDEVTOOLS'), role: 'toggleDevTools' }, 12 | { type: 'separator' }, 13 | { label: i18next.t('MENU_VIEW_RESETZOOM'), role: 'resetZoom' }, 14 | { label: i18next.t('MENU_VIEW_ZOOMIN'), role: 'zoomIn' }, 15 | { label: i18next.t('MENU_VIEW_ZOOMOUT'), role: 'zoomOut' }, 16 | { type: 'separator' }, 17 | { label: i18next.t('MENU_VIEW_FULLSCREEN'), role: 'togglefullscreen' }, 18 | ], 19 | }; 20 | }; 21 | 22 | export default builder; 23 | -------------------------------------------------------------------------------- /kmfrontend/src/utils/components/Loading.tsx: -------------------------------------------------------------------------------- 1 | import './Loading.scss'; 2 | 3 | import i18next from 'i18next'; 4 | import { useEffect, useState } from 'react'; 5 | 6 | import { isElectron } from '../electron'; 7 | 8 | function Loading() { 9 | const [showLoadingText, setShowLoadingText] = useState(false); 10 | 11 | let timeout: NodeJS.Timeout; 12 | 13 | useEffect(() => { 14 | timeout = setTimeout(() => setShowLoadingText(true), 1000); 15 | return () => { 16 | clearTimeout(timeout); 17 | }; 18 | }, []); 19 | 20 | return ( 21 | <div className="loading-container"> 22 | {showLoadingText ? ( 23 | <> 24 | <span className="header">{i18next.t('LOADING')}</span> 25 | <span>{isElectron() ? i18next.t('LOADING_SUBTITLE_ELECTRON') : i18next.t('LOADING_SUBTITLE')}</span> 26 | </> 27 | ) : null} 28 | </div> 29 | ); 30 | } 31 | 32 | export default Loading; 33 | -------------------------------------------------------------------------------- /migrations/20210712000000.do.renameSephirothGuest.sql: -------------------------------------------------------------------------------- 1 | UPDATE users SET pk_login = 'sephir0th69' WHERE pk_login = 's€phir0th69'; 2 | UPDATE users SET pk_login = 'hooin kyoma' WHERE pk_login = 'hôôin kyoma'; 3 | UPDATE users SET pk_login = 'blue accordeon' WHERE pk_login = 'blue accordéon'; 4 | UPDATE users SET pk_login = 'une simple reveuse' WHERE pk_login = 'une simple rêveuse'; 5 | UPDATE users SET pk_login = 'kumiko omae' WHERE pk_login = 'kumiko ômae'; 6 | UPDATE users SET pk_login = 'kamel deux baches' WHERE pk_login = 'kamel deux bâches'; 7 | UPDATE users SET pk_login = 'silent mobius dvd' WHERE pk_login = 'silent möbius dvd'; 8 | UPDATE users SET pk_login = 'eren jager' WHERE pk_login = 'eren jäger'; 9 | UPDATE users SET pk_login = 'livai' WHERE pk_login = 'livaï'; 10 | UPDATE users SET pk_login = 'rodeur de la nuit' WHERE pk_login = 'rôdeur de la nuit'; 11 | UPDATE users SET pk_login = 'poi' WHERE pk_login = 'poï'; -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .idea/ 3 | .npmrc 4 | .vscode/settings.json 5 | .vs 6 | node_modules 7 | app/ 8 | app-mac/ 9 | 10 | npm-debug.log* 11 | yarn-debug.log* 12 | yarn-error.log* 13 | install-state.gz 14 | *.exe 15 | karaokemugen*.log 16 | out/ 17 | doc/ 18 | \.DS_Store 19 | *.backup 20 | config\.yml 21 | *.bak 22 | karaokemugen-app-linux 23 | karaokemugen-app-macos 24 | dist/ 25 | bin/ 26 | logs/ 27 | updater/ 28 | mpv.log 29 | karaokemugen.pgdump 30 | karaokemugen.sql 31 | karaokemugen.sql.gz 32 | sentry.txt 33 | sha.txt 34 | 35 | # kmfrontend 36 | kmfrontend/node_modules 37 | kmfrontend/coverage 38 | 39 | # sessions export 40 | *.played.csv 41 | *.playedCount.csv 42 | *.requested.csv 43 | *.requestedCount.csv 44 | dist2 45 | js 46 | packages 47 | postgrator.json 48 | 49 | # Dev 50 | .env 51 | .eslintcache 52 | 53 | # Flatpak 54 | .flatpak-builder 55 | build/export 56 | *.flatpak 57 | build/build 58 | build/km 59 | repo/ 60 | -------------------------------------------------------------------------------- /kmfrontend/src/store/types/auth.ts: -------------------------------------------------------------------------------- 1 | // Action name 2 | export enum AuthAction { 3 | LOGIN_SUCCESS = 'login_success', 4 | LOGIN_FAILURE = 'login_failure', 5 | LOGOUT_USER = 'logout_user', 6 | } 7 | 8 | // Dispatch action 9 | export interface LoginSuccess { 10 | type: AuthAction.LOGIN_SUCCESS; 11 | payload: IAuthentifactionInformation; 12 | } 13 | 14 | export interface LoginFailure { 15 | type: AuthAction.LOGIN_FAILURE; 16 | payload: { 17 | error: string; 18 | }; 19 | } 20 | 21 | export interface LogoutUser { 22 | type: AuthAction.LOGOUT_USER; 23 | } 24 | 25 | // Store 26 | 27 | export interface AuthStore { 28 | data: IAuthentifactionInformation; 29 | isAuthenticated: boolean; 30 | error: string; 31 | } 32 | 33 | export interface IAuthentifactionInformation { 34 | token: string; 35 | onlineToken?: string; 36 | username: string; 37 | role: string; 38 | onlineAvailable?: boolean; 39 | } 40 | -------------------------------------------------------------------------------- /kmfrontend/src/frontend/components/public/TagsList.scss: -------------------------------------------------------------------------------- 1 | @use 'sass:color'; 2 | @use '../../styles/variables'; 3 | 4 | .tags-list { 5 | display: inline-flex; 6 | position: relative; 7 | flex: 1 0 auto; 8 | flex-direction: column; 9 | div.tags-item { 10 | padding: 1em; 11 | background-color: variables.$mugen-playlist-odd; 12 | transition: background-color 250ms ease; 13 | cursor: pointer; 14 | &:hover, 15 | &:focus { 16 | background-color: color.adjust(variables.$mugen-playlist-odd, $lightness: 5%, $space: hsl); 17 | } 18 | &.even { 19 | background-color: variables.$mugen-playlist-even; 20 | &:hover, 21 | &:focus { 22 | background-color: color.adjust(variables.$mugen-playlist-even, $lightness: 5%, $space: hsl); 23 | } 24 | } 25 | > .title { 26 | font-size: 1.5em; 27 | font-weight: bold; 28 | } 29 | > .karacount { 30 | font-weight: lighter; 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/controllers/middlewaresHTTP.ts: -------------------------------------------------------------------------------- 1 | import { APIMessage } from '../lib/services/frontend.js'; 2 | import logger from '../lib/utils/logger.js'; 3 | import { decodeJwtToken } from '../services/user.js'; 4 | import { checkValidUser } from './middlewares.js'; 5 | 6 | export function requireHTTPAuth(req: any, res: any, next: any) { 7 | if (req.get('authorization')) { 8 | req.token = decodeJwtToken(req.get('authorization')); 9 | next(); 10 | } else { 11 | res.status(401).json(APIMessage('USER_UNKNOWN')); 12 | } 13 | } 14 | 15 | export function requireValidUser(req: any, res: any, next: any) { 16 | req.authToken = req.token; 17 | checkValidUser(req.token) 18 | .then(user => { 19 | req.user = user; 20 | next(); 21 | }) 22 | .catch(err => { 23 | logger.error(`Error checking user : ${JSON.stringify(req.token)}`, { service: 'API', obj: err }); 24 | res.status(403).json(APIMessage('USER_UNKNOWN')); 25 | }); 26 | } 27 | -------------------------------------------------------------------------------- /kmfrontend/src/store/reducers/settings.ts: -------------------------------------------------------------------------------- 1 | import { Settings, SettingsFailure, SettingsStore, SettingsSuccess } from '../types/settings'; 2 | 3 | export const initialStateConfig: SettingsStore = { 4 | data: { 5 | state: undefined, 6 | config: undefined, 7 | user: undefined, 8 | favorites: undefined, 9 | version: undefined, 10 | }, 11 | error: '', 12 | }; 13 | 14 | export default function settingsReducer(state, action: SettingsSuccess | SettingsFailure) { 15 | switch (action.type) { 16 | case Settings.SETTINGS_SUCCESS: 17 | return { 18 | ...state, 19 | data: { 20 | ...action.payload, 21 | }, 22 | error: '', 23 | }; 24 | case Settings.SETTINGS_FAILURE: 25 | return { 26 | ...state, 27 | // Let the old data persists, as it will cause trouble with many components that except full objects. 28 | error: action.payload.error, 29 | }; 30 | default: 31 | return state; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /kmfrontend/src/frontend/styles/components/dropdowns.scss: -------------------------------------------------------------------------------- 1 | @use '../variables'; 2 | 3 | .dropdown-menu { 4 | background-color: variables.$mugen-btn-background; 5 | border: variables.$mugen-btn-border; 6 | cursor: pointer; 7 | position: absolute; 8 | list-style: none; 9 | padding: 5px 0; 10 | z-index: 60; 11 | } 12 | .dropdown-menu > li { 13 | padding-top: 5px; 14 | padding-bottom: 5px; 15 | } 16 | .dropdown-menu > li > a, 17 | .dropdown-menu > li > div:not(.radiobutton-ui) { 18 | display: inline-block; 19 | width: 100%; 20 | padding: 0.2rem 0.5rem; 21 | color: #eee; 22 | // Ant Design overrides 23 | transition: none; 24 | text-decoration: none; 25 | } 26 | 27 | .dropdown-menu > li > a i, 28 | .dropdown-menu > li > div:not(.radiobutton-ui) i { 29 | margin-right: 0.5em; 30 | } 31 | .dropdown-menu > li:hover > a, 32 | .dropdown-menu > li:hover > div:not(.radiobutton-ui) { 33 | background-color: variables.$mugen-select-focus; 34 | } 35 | -------------------------------------------------------------------------------- /kmfrontend/src/store/types/settings.ts: -------------------------------------------------------------------------------- 1 | import { User } from '../../../../src/lib/types/user'; 2 | import { Config } from '../../../../src/types/config'; 3 | import { PublicState, Version } from '../../../../src/types/state'; 4 | 5 | // Action name 6 | export enum Settings { 7 | SETTINGS_SUCCESS = 'settings_success', 8 | SETTINGS_FAILURE = 'settings_failure', 9 | } 10 | 11 | // Dispatch action 12 | export interface SettingsSuccess { 13 | type: Settings.SETTINGS_SUCCESS; 14 | payload: SettingsStoreData; 15 | } 16 | 17 | export interface SettingsFailure { 18 | type: Settings.SETTINGS_FAILURE; 19 | payload: { 20 | error: string; 21 | }; 22 | } 23 | 24 | // Store 25 | 26 | export interface SettingsStore { 27 | data: SettingsStoreData; 28 | error: string; 29 | } 30 | 31 | export interface SettingsStoreData { 32 | state: PublicState; 33 | config: Config; 34 | user?: User; 35 | favorites: Set<string>; 36 | version: Version; 37 | } 38 | -------------------------------------------------------------------------------- /kmfrontend/src/systempanel/components/Loading.tsx: -------------------------------------------------------------------------------- 1 | import { Alert, Spin } from 'antd'; 2 | import i18next from 'i18next'; 3 | import { Component } from 'react'; 4 | 5 | import { eventEmitter } from '../../utils/tools'; 6 | 7 | class Loading extends Component<unknown, unknown> { 8 | state = { 9 | loading: false, 10 | }; 11 | 12 | componentDidMount() { 13 | eventEmitter.addChangeListener('loading', this.setLoading); 14 | } 15 | 16 | componentWillUnmount() { 17 | eventEmitter.removeChangeListener('loading', this.setLoading); 18 | } 19 | 20 | setLoading = loading => { 21 | this.setState({ loading }); 22 | }; 23 | 24 | render() { 25 | return this.state.loading ? ( 26 | <div className="UI-notification-loading"> 27 | <Spin tip={i18next.t('LOADING')}> 28 | <Alert message="Loading" description="Please wait..." type="info" /> 29 | </Spin> 30 | </div> 31 | ) : null; 32 | } 33 | } 34 | 35 | export default Loading; 36 | -------------------------------------------------------------------------------- /kmfrontend/src/systempanel/components/OpenLyricsFileButton.tsx: -------------------------------------------------------------------------------- 1 | import { EditOutlined } from '@ant-design/icons'; 2 | import { Button } from 'antd'; 3 | import i18next from 'i18next'; 4 | 5 | import { DBKara } from '../../../../src/lib/types/database/kara'; 6 | import { commandBackend } from '../../utils/socket'; 7 | import { WS_CMD } from '../../utils/ws'; 8 | 9 | interface IProps { 10 | kara: DBKara; 11 | showOnlyIfDownloaded?: boolean; 12 | } 13 | 14 | export default function OpenLyricsFileButton({ kara, showOnlyIfDownloaded = true }: IProps): JSX.Element { 15 | const { kid, download_status } = kara; 16 | if (!showOnlyIfDownloaded || download_status === 'DOWNLOADED') { 17 | return ( 18 | <Button 19 | type="primary" 20 | icon={<EditOutlined />} 21 | onClick={() => commandBackend(WS_CMD.OPEN_LYRICS_FILE, { kid: kid }).catch(() => {})} 22 | > 23 | {i18next.t('KARA.LYRICS_FILE_OPEN')} 24 | </Button> 25 | ); 26 | } 27 | return null; 28 | } 29 | -------------------------------------------------------------------------------- /kmfrontend/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "sourceMap": true, 4 | "inlineSources": true, 5 | "sourceRoot": "/", 6 | "target": "es2021", 7 | "jsx": "react-jsx", 8 | "module": "esnext", 9 | "moduleResolution": "node", 10 | "emitDecoratorMetadata": true, 11 | "experimentalDecorators": true, 12 | "declaration": false, 13 | "noImplicitAny": false, 14 | "noImplicitReturns": false, 15 | "noFallthroughCasesInSwitch": true, 16 | "removeComments": true, 17 | "strictNullChecks": false, 18 | "outDir": "build", 19 | "types": [], 20 | "lib": ["dom", "es2021"], 21 | "allowJs": true, 22 | "skipLibCheck": true, 23 | "esModuleInterop": true, 24 | "allowSyntheticDefaultImports": true, 25 | "strict": true, 26 | "forceConsistentCasingInFileNames": true, 27 | "resolveJsonModule": true, 28 | "isolatedModules": true, 29 | "noEmit": true 30 | }, 31 | "exclude": ["dist", "build", "node_modules"], 32 | "include": ["src"] 33 | } 34 | -------------------------------------------------------------------------------- /kmfrontend/src/frontend/components/public/QuizPage.scss: -------------------------------------------------------------------------------- 1 | @use 'sass:color'; 2 | @use '../../styles/variables'; 3 | 4 | .scores-popup { 5 | display: none; 6 | &.active { 7 | display: initial; 8 | } 9 | position: absolute; 10 | cursor: initial; 11 | user-select: initial; 12 | top: 200%; 13 | left: 50%; 14 | transform: translateX(-50%); 15 | width: max-content; 16 | max-width: 95vw; 17 | max-height: calc(95vh - var(--top, 0)); 18 | z-index: 10; 19 | 20 | padding: 1em; 21 | border-radius: 16px; 22 | 23 | background-color: color.scale(variables.$mugen-background, $lightness: -75%); 24 | font-size: 1.125rem; 25 | 26 | > hr { 27 | margin: 0.5em 0 0.25em; 28 | } 29 | 30 | > div { 31 | display: flex; 32 | justify-content: space-between; 33 | gap: 1em; 34 | } 35 | } 36 | 37 | details.rules { 38 | p { 39 | margin: 0.25em 0; 40 | } 41 | ul > li { 42 | margin-left: 0.75ch; 43 | i { 44 | padding: 0 1ch; 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /test/auth.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | 3 | import { commandBackend, passwordAdmin, usernameAdmin } from './util/util.js'; 4 | 5 | describe('Auth', () => { 6 | it('Login / Sign in (as guest)', async () => { 7 | const data = await commandBackend(undefined, 'loginGuest'); 8 | expect(data.role).to.be.equal('guest'); 9 | }); 10 | it('Login / Sign in', async () => { 11 | const data = await commandBackend(undefined, 'login', { 12 | username: usernameAdmin, 13 | password: passwordAdmin, 14 | }); 15 | expect(data.role).to.be.equal('admin'); 16 | expect(data.username).to.be.equal(data.username); 17 | }); 18 | 19 | it('Login / Sign in Error 401', async () => { 20 | try { 21 | await commandBackend( 22 | undefined, 23 | 'login', 24 | { 25 | username: '', 26 | password: '', 27 | }, 28 | true 29 | ); 30 | } catch (err) { 31 | console.log(err); 32 | expect(err.code).to.be.equal(401); 33 | } 34 | }); 35 | }); 36 | -------------------------------------------------------------------------------- /src/electron/menus/tools.ts: -------------------------------------------------------------------------------- 1 | import i18next from 'i18next'; 2 | 3 | import { MenuItemBuilderFunction } from '../../types/electron.js'; 4 | import { urls } from './index.js'; 5 | 6 | const builder: MenuItemBuilderFunction = options => { 7 | const { layout } = options; 8 | if (layout === 'REDUCED') { 9 | return null; 10 | } 11 | return { 12 | label: i18next.t('MENU_TOOLS'), 13 | submenu: [ 14 | { 15 | label: i18next.t('MENU_TOOLS_LOGS'), 16 | accelerator: 'CmdOrCtrl+L', 17 | click: urls.logs, 18 | }, 19 | { 20 | label: i18next.t('MENU_TOOLS_DOWNLOADS'), 21 | accelerator: 'CmdOrCtrl+D', 22 | click: urls.download, 23 | }, 24 | { 25 | label: i18next.t('MENU_TOOLS_KARAOKES'), 26 | accelerator: 'CmdOrCtrl+K', 27 | click: urls.karas, 28 | }, 29 | { 30 | label: i18next.t('MENU_TOOLS_DATABASE'), 31 | accelerator: 'CmdOrCtrl+B', 32 | click: urls.database, 33 | }, 34 | ], 35 | }; 36 | }; 37 | 38 | export default builder; 39 | -------------------------------------------------------------------------------- /src/types/repo.d.ts: -------------------------------------------------------------------------------- 1 | import { KaraFileV4 } from '../lib/types/kara.js'; 2 | 3 | export interface DifferentChecksumReport { 4 | kara1: KaraFileV4; 5 | kara2: KaraFileV4; 6 | } 7 | 8 | export interface Change { 9 | type: 'new' | 'delete'; 10 | path: string; 11 | uid?: string; 12 | } 13 | 14 | export interface Push { 15 | commits: Commit[]; 16 | modifiedMedias: ModifiedMedia[]; 17 | squash?: string; 18 | } 19 | 20 | export interface Commit { 21 | addedFiles: string[]; 22 | removedFiles: string[]; 23 | message: string; 24 | } 25 | 26 | export interface ModifiedMedia { 27 | new: string; 28 | old: string; 29 | sizeDifference?: boolean; 30 | commit: string; 31 | } 32 | 33 | export interface ImportKaraObject { 34 | [TagType: string]: string; 35 | title: string; 36 | year?: number; 37 | } 38 | 39 | export interface ImportBaseFile { 40 | directory: string; 41 | oldFile: string; 42 | newFile: ImportKaraObject; 43 | tags: { 44 | [TagType: string]: string[]; 45 | }; 46 | } 47 | -------------------------------------------------------------------------------- /migrations/20230615000000.do.addFallbackPlaylistType.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE playlist ADD COLUMN IF NOT EXISTS flag_fallback BOOLEAN; 2 | UPDATE playlist SET flag_fallback = false; 3 | 4 | 5 | CREATE OR REPLACE FUNCTION ensure_only_one_enabled_fallback_trigger() 6 | RETURNS trigger 7 | AS $function$ 8 | BEGIN 9 | -- nothing to do if updating the row currently enabled 10 | IF (TG_OP = 'UPDATE' AND OLD.flag_fallback = true) THEN 11 | RETURN NEW; 12 | END IF; 13 | 14 | -- disable the currently enabled row 15 | EXECUTE format('UPDATE %I.%I SET flag_fallback = false WHERE flag_fallback = true;', TG_TABLE_SCHEMA, TG_TABLE_NAME); 16 | 17 | -- enable new row 18 | NEW.flag_fallback := true; 19 | RETURN NEW; 20 | END; 21 | $function$ 22 | LANGUAGE plpgsql; 23 | 24 | 25 | CREATE TRIGGER playlist_only_one_fallback_trigger 26 | BEFORE INSERT OR UPDATE OF flag_fallback ON playlist 27 | FOR EACH ROW WHEN (NEW.flag_fallback = true) 28 | EXECUTE PROCEDURE ensure_only_one_enabled_fallback_trigger(); -------------------------------------------------------------------------------- /kmfrontend/src/systempanel/pages/Tags/TagsNew.tsx: -------------------------------------------------------------------------------- 1 | import { Layout } from 'antd'; 2 | import i18next from 'i18next'; 3 | import { useNavigate } from 'react-router-dom'; 4 | 5 | import { Tag } from '../../../../../src/lib/types/tag'; 6 | import { commandBackend } from '../../../utils/socket'; 7 | import Title from '../../components/Title'; 8 | import TagsForm from './TagsForm'; 9 | import { WS_CMD } from '../../../utils/ws'; 10 | 11 | function TagNew() { 12 | const navigate = useNavigate(); 13 | 14 | const saveNew = async (tag: Tag) => { 15 | try { 16 | await commandBackend(WS_CMD.ADD_TAG, tag, true, 300000); 17 | navigate('/system/tags'); 18 | } catch (_) { 19 | // already display 20 | } 21 | }; 22 | 23 | return ( 24 | <> 25 | <Title title={i18next.t('HEADERS.TAG_NEW.TITLE')} description={i18next.t('HEADERS.TAG_NEW.DESCRIPTION')} /> 26 | <Layout.Content> 27 | <TagsForm tags={[]} save={saveNew} /> 28 | </Layout.Content> 29 | </> 30 | ); 31 | } 32 | 33 | export default TagNew; 34 | -------------------------------------------------------------------------------- /migrations/20220425000000.do.reworkTagViewCollections.sql: -------------------------------------------------------------------------------- 1 | DROP MATERIALIZED VIEW all_tags; 2 | 3 | CREATE TABLE all_tags AS ( 4 | WITH kara_available AS ( 5 | SELECT k.pk_kid 6 | FROM kara k 7 | LEFT JOIN kara_tag kt ON k.pk_kid = kt.fk_kid 8 | --WHERE kt.fk_tid = 'efe171c0-e8a1-4d03-98c0-60ecf741ad52' 9 | --We're not putting a where yet, this will be refreshed on first startup. 10 | ), 11 | t_count AS ( 12 | SELECT a.fk_tid, 13 | json_agg(json_build_object('type', a.type, 'count', a.c))::text AS count_per_type 14 | FROM (SELECT kara_tag.fk_tid, 15 | count(kara_tag.fk_kid) AS c, 16 | kara_tag.type 17 | FROM kara_tag 18 | WHERE kara_tag.fk_kid IN (SELECT * FROM kara_available) 19 | GROUP BY kara_tag.fk_tid, kara_tag.type) a 20 | GROUP BY a.fk_tid 21 | ) 22 | 23 | select t.*, 24 | t_count.count_per_type::jsonb AS karacount 25 | from tag t 26 | LEFT JOIN t_count ON t.pk_tid = t_count.fk_tid 27 | 28 | ); 29 | 30 | CREATE UNIQUE INDEX idx_at_tid 31 | on all_tags (pk_tid); 32 | -------------------------------------------------------------------------------- /src/electron/menus/goTo.ts: -------------------------------------------------------------------------------- 1 | import i18next from 'i18next'; 2 | 3 | import { MenuItemBuilderFunction } from '../../types/electron.js'; 4 | import { urls } from './index.js'; 5 | 6 | const builder: MenuItemBuilderFunction = options => { 7 | const { isMac, layout } = options; 8 | if (layout === 'REDUCED') { 9 | return null; 10 | } 11 | return { 12 | label: isMac ? i18next.t('MENU_GOTO_OSX') : i18next.t('MENU_GOTO'), 13 | submenu: [ 14 | { 15 | label: i18next.t('MENU_GOTO_HOME'), 16 | accelerator: 'CmdOrCtrl+H', 17 | click: urls.home, 18 | }, 19 | { 20 | label: i18next.t('MENU_GOTO_OPERATOR'), 21 | accelerator: 'CmdOrCtrl+O', 22 | click: urls.operator, 23 | }, 24 | { 25 | label: i18next.t('MENU_GOTO_SYSTEM'), 26 | accelerator: 'CmdOrCtrl+S', 27 | click: urls.system, 28 | }, 29 | { 30 | label: i18next.t('MENU_GOTO_PUBLIC'), 31 | accelerator: 'CmdOrCtrl+P', 32 | click: urls.public, 33 | }, 34 | ], 35 | }; 36 | }; 37 | 38 | export default builder; 39 | -------------------------------------------------------------------------------- /kmfrontend/src/frontend/components/generic/RadioButton.tsx: -------------------------------------------------------------------------------- 1 | import './RadioButton.scss'; 2 | 3 | interface Button { 4 | activeColor: string; 5 | active: boolean; 6 | onClick: () => void; 7 | label: string; 8 | description?: string; 9 | } 10 | 11 | interface IProps { 12 | orientation?: string; 13 | buttons: Button[]; 14 | } 15 | 16 | function RadioButton(props: IProps) { 17 | return ( 18 | <div className="radiobutton-ui" data-orientation={props.orientation || 'horizontal'}> 19 | {props.buttons.map((item: Button, i: number) => { 20 | const style: any = {}; 21 | if (item.active && item.activeColor) style.backgroundColor = item.activeColor; 22 | return ( 23 | <button 24 | title={item.description} 25 | key={i} 26 | type="button" 27 | className={item.active ? 'active' : ''} 28 | style={style} 29 | onClick={item.onClick} 30 | > 31 | {item.label} 32 | </button> 33 | ); 34 | })} 35 | </div> 36 | ); 37 | } 38 | 39 | export default RadioButton; 40 | -------------------------------------------------------------------------------- /util/extUnaccent.js: -------------------------------------------------------------------------------- 1 | // This script installs the unaccent extension in a database. 2 | // It requires superuser access. 3 | 4 | import { readFileSync } from 'fs'; 5 | import { load } from 'js-yaml'; 6 | import pg from 'pg'; 7 | 8 | async function main() { 9 | const configFile = readFileSync('app/config.yml', 'utf-8'); 10 | const config = load(configFile); 11 | const dbConfig = { 12 | host: config.System.Database.host, 13 | user: config.System.Database.username, 14 | port: config.System.Database.port, 15 | password: config.System.Database.password, 16 | database: config.System.Database.database, 17 | }; 18 | const client = new pg.Pool(dbConfig); 19 | await client.connect(); 20 | try { 21 | await client.query(` 22 | CREATE EXTENSION unaccent; 23 | `); 24 | } catch (err) { 25 | // Do nothing here. 26 | } 27 | } 28 | 29 | main() 30 | .then(() => { 31 | console.log('unaccent extension installed'); 32 | process.exit(0); 33 | }) 34 | .catch(err => { 35 | console.log(err); 36 | process.exit(1); 37 | }); 38 | -------------------------------------------------------------------------------- /src/electron/electronLogger.ts: -------------------------------------------------------------------------------- 1 | import i18next from 'i18next'; 2 | import Transport from 'winston-transport'; 3 | 4 | import { setTipLoop } from '../utils/tips.js'; 5 | import { win } from './electron.js'; 6 | 7 | let errorHappened = false; 8 | 9 | export function initStep(step: string, lastEvent?: boolean) { 10 | emitIPC('initStep', { message: step, lastEvent }); 11 | } 12 | 13 | export function errorStep(step: string) { 14 | // Not triggering if one error already happened 15 | if (win && !errorHappened) { 16 | errorHappened = true; 17 | initStep(i18next.t('INIT_ERROR')); 18 | setTipLoop('errors'); 19 | emitIPC('error', { message: step }); 20 | } 21 | } 22 | 23 | export function emitIPC(type: string, data: any) { 24 | if (win) win.webContents.send(type, data); 25 | } 26 | 27 | export class IPCTransport extends Transport { 28 | log(info: any, callback: any) { 29 | try { 30 | emitIPC('log', info); 31 | } catch (err) { 32 | // Non fatal. We can safely ignore 33 | } finally { 34 | callback(); 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /migrations/20191026124159.do.AddTagKaracountWithType.sql: -------------------------------------------------------------------------------- 1 | DROP MATERIALIZED VIEW all_tags; 2 | CREATE MATERIALIZED VIEW all_tags AS 3 | WITH t_count as ( 4 | select a.fk_tid, json_agg(json_build_object('type', a.type, 'count', a.c))::text AS count_per_type 5 | FROM ( 6 | SELECT fk_tid, count(fk_kid) as c, type 7 | FROM kara_tag 8 | GROUP BY fk_tid, type) as a 9 | GROUP BY a.fk_tid 10 | ) 11 | SELECT 12 | t.name AS name, 13 | t.types AS types, 14 | t.aliases AS aliases, 15 | t.i18n AS i18n, 16 | t.pk_tid AS tid, 17 | tag_aliases.list AS search_aliases, 18 | t.tagfile AS tagfile, 19 | t.short as short, 20 | count_per_type::jsonb AS karacount 21 | FROM tag t 22 | CROSS JOIN LATERAL ( 23 | SELECT string_agg(tag_aliases.elem::text, ' ') AS list 24 | FROM jsonb_array_elements_text(t.aliases) AS tag_aliases(elem) 25 | ) tag_aliases 26 | LEFT JOIN t_count on t.pk_tid = t_count.fk_tid 27 | GROUP BY t.pk_tid, tag_aliases.list, count_per_type 28 | ORDER BY name; 29 | -------------------------------------------------------------------------------- /src/types/download.d.ts: -------------------------------------------------------------------------------- 1 | export type QueueStatus = 'started' | 'stopped' | 'paused' | 'updated'; 2 | 3 | export interface KaraDownload { 4 | name: string; 5 | mediafile: string; 6 | size: number; 7 | uuid: string; 8 | status?: 'DL_RUNNING' | 'DL_PLANNED' | 'DL_DONE' | 'DL_FAILED'; 9 | repository: string; 10 | kid: string; 11 | } 12 | 13 | export interface KaraDownloadRequest { 14 | mediafile: string; 15 | name: string; 16 | size: number; 17 | repository: string; 18 | kid: string; 19 | } 20 | 21 | interface DownloadFile { 22 | remote: string; 23 | local: string; 24 | } 25 | 26 | export interface File { 27 | basename: string; 28 | size: number; 29 | kid?: string; 30 | } 31 | 32 | export interface MediaDownloadCheck { 33 | kid: string; 34 | mediafile: string; 35 | repository: string; 36 | mediasize: number; 37 | songname: string; 38 | } 39 | 40 | export interface UpdateMediasResult { 41 | removedFiles: string[]; 42 | addedFiles: DBMedia[]; 43 | updatedFiles: DBMedia[]; 44 | repoName: string; 45 | bytesToDownload: number; 46 | } 47 | -------------------------------------------------------------------------------- /migrations/20200329131617.do.addUserConstraints.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE favorites DROP CONSTRAINT favorites_fk_login_fkey; 2 | 3 | UPDATE favorites SET fk_login = COALESCE((SELECT u.pk_login FROM users u WHERE u.pk_login = fk_login), 'admin'); 4 | UPDATE playlist_content SET fk_login = COALESCE((SELECT u.pk_login FROM users u WHERE u.pk_login = fk_login), 'admin'); 5 | UPDATE requested SET fk_login = COALESCE((SELECT u.pk_login FROM users u WHERE u.pk_login = fk_login), 'admin'); 6 | UPDATE upvote SET fk_login = COALESCE((SELECT u.pk_login FROM users u WHERE u.pk_login = fk_login), 'admin'); 7 | 8 | ALTER TABLE favorites ADD CONSTRAINT favorites_login_fkey FOREIGN KEY(fk_login) REFERENCES users(pk_login) ON DELETE CASCADE ON UPDATE CASCADE; 9 | ALTER TABLE playlist_content ADD CONSTRAINT plc_login_fkey FOREIGN KEY(fk_login) REFERENCES users(pk_login) ON UPDATE CASCADE; 10 | ALTER TABLE requested ADD CONSTRAINT requested_login_fkey FOREIGN KEY(fk_login) REFERENCES users(pk_login) ON UPDATE CASCADE; 11 | ALTER TABLE upvote ADD CONSTRAINT upvote_login_fkey FOREIGN KEY(fk_login) REFERENCES users(pk_login) ON DELETE CASCADE ON UPDATE CASCADE; -------------------------------------------------------------------------------- /migrations/20210422233728.do.goToPlaids.sql: -------------------------------------------------------------------------------- 1 | alter table playlist_content 2 | drop constraint playlist_content_fk_id_playlist_fkey; 3 | 4 | alter table playlist 5 | alter column pk_id_playlist type text; 6 | 7 | alter table playlist_content 8 | alter column fk_id_playlist type text, 9 | add constraint playlist_content_fk_id_playlist_fkey 10 | foreign key (fk_id_playlist) references playlist 11 | on update cascade on delete cascade; 12 | 13 | update playlist set pk_id_playlist = gen_random_uuid()::text; 14 | 15 | alter table playlist_content 16 | drop constraint playlist_content_fk_id_playlist_fkey; 17 | 18 | alter table playlist 19 | alter column pk_id_playlist set default gen_random_uuid(); 20 | 21 | alter table playlist 22 | alter column pk_id_playlist type uuid using pk_id_playlist::uuid; 23 | 24 | alter table playlist_content 25 | alter column fk_id_playlist type uuid using fk_id_playlist::uuid, 26 | add constraint playlist_content_fk_id_playlist_fkey 27 | foreign key (fk_id_playlist) references playlist 28 | on delete cascade; 29 | -------------------------------------------------------------------------------- /kmfrontend/index.html: -------------------------------------------------------------------------------- 1 | <!doctype html> 2 | <html lang="en"> 3 | <head> 4 | <meta charset="utf-8" /> 5 | <meta 6 | name="viewport" 7 | content="width=device-width, initial-scale=1.0, shrink-to-fit=no, maximum-scale=1.0, user-scalable=no" 8 | /> 9 | <meta name="theme-color" content="#000000" /> 10 | <meta name="target" content="NO-REMOTE" /> 11 | <!-- 12 | manifest.json provides metadata used when your web app is added to the 13 | homescreen on Android. See https://developers.google.com/web/fundamentals/engage-and-retain/web-app-manifest/ 14 | --> 15 | <link rel="manifest" href="/manifest.json" /> 16 | <link rel="shortcut icon" href="/favicon.ico" /> 17 | <title>Karaoke Mugen 18 | 23 | 24 | 25 | 26 |
27 |
28 |
29 |
30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /migrations/20240527000002.do.fixSingergroupsSortablePart3.sql: -------------------------------------------------------------------------------- 1 | create index idx_ak_search_vector 2 | on all_karas using gin (search_vector); 3 | 4 | create index idx_ak_created 5 | on all_karas (created_at desc); 6 | 7 | create index idx_ak_songtypes 8 | on all_karas (songtypes_sortable desc); 9 | 10 | create index idx_ak_songorder 11 | on all_karas (songorder); 12 | 13 | create index idx_ak_title 14 | on all_karas (titles_sortable); 15 | 16 | create index idx_ak_series_singergroups_singers 17 | on all_karas (serie_singergroup_singer_sortable); 18 | 19 | create index idx_ak_language 20 | on all_karas (languages_sortable); 21 | 22 | create index idx_ak_year 23 | on all_karas (year); 24 | 25 | create UNIQUE index idx_ak_kid 26 | on all_karas (pk_kid); 27 | 28 | create index idx_ak_search_vector_parents 29 | on all_karas using gin (search_vector_parents); 30 | 31 | create index idx_ak_anilist 32 | on all_karas (anilist_ids); 33 | 34 | create index idx_ak_kitsu 35 | on all_karas (kitsu_ids); 36 | 37 | create index idx_ak_myanimelist 38 | on all_karas (myanimelist_ids); 39 | -------------------------------------------------------------------------------- /src/types/database/playlist.d.ts: -------------------------------------------------------------------------------- 1 | import { DBPLBase, DBPLC } from '../../lib/types/database/playlist.js'; 2 | 3 | export interface DBPL extends DBPLBase { 4 | time_left?: number; 5 | time_played?: number; 6 | songs_left?: number; 7 | songs_played?: number; 8 | plcid_playing?: number; 9 | flag_current?: boolean; 10 | flag_public?: boolean; 11 | flag_whitelist?: boolean; 12 | flag_blacklist?: boolean; 13 | flag_fallback?: boolean; 14 | flag_smart?: boolean; 15 | type_smart?: SmartPlaylistType; 16 | flag_smartlimit?: boolean; 17 | smart_limit_order?: SmartPlaylistLimitOrder; 18 | smart_limit_type?: SmartPlaylistLimitType; 19 | smart_limit_number?: number; 20 | } 21 | 22 | export interface DBPLPos { 23 | pos: number; 24 | plcid: number; 25 | } 26 | 27 | export interface DBPLKidUser extends DBPLPos { 28 | flag_playing: boolean; 29 | } 30 | 31 | export interface DBPLCInfo extends DBPLC { 32 | time_before_play: number; 33 | } 34 | 35 | export type SmartPlaylistLimitType = 'songs' | 'duration'; 36 | export type SmartPlaylistLimitOrder = 'newest' | 'oldest'; 37 | export type SmartPlaylistType = 'UNION' | 'INTERSECT'; 38 | -------------------------------------------------------------------------------- /util/sentryUpdateReleases.js: -------------------------------------------------------------------------------- 1 | import { promises as fs } from 'node:fs'; 2 | 3 | import SentryCli from '@sentry/cli'; 4 | 5 | const { version } = JSON.parse(await fs.readFile('package.json', 'utf-8')); 6 | 7 | const sentry = new SentryCli(null, { 8 | authToken: process.env.SENTRYTOKEN, 9 | org: 'karaoke-mugen', 10 | }); 11 | 12 | const dist = process.env.CI_COMMIT_SHORT_SHA; 13 | 14 | await sentry.releases.new(version, { projects: ['km-app'] }); 15 | await sentry.releases.uploadSourceMaps(version, { 16 | rewrite: false, 17 | urlPrefix: 'app:///dist/', 18 | include: ['dist/'], 19 | projects: ['km-app'], 20 | ext: ['.cjs', '.map'], 21 | dist, 22 | }); 23 | await sentry.releases.uploadSourceMaps(version, { 24 | rewrite: false, 25 | urlPrefix: '~/assets', 26 | include: ['kmfrontend/dist/assets'], 27 | projects: ['km-app'], 28 | }); 29 | await sentry.releases.setCommits(version, { 30 | repo: 'Karaoke Mugen / Code / Karaoke Mugen Application', 31 | commit: process.env.CI_COMMIT_SHA, 32 | }); 33 | 34 | if (process.env.CI_COMMIT_TAG) { 35 | await sentry.releases.newDeploy(version, { 36 | env: 'release', 37 | }); 38 | } 39 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) Karaoke Mugen 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/electron/menus/edit.ts: -------------------------------------------------------------------------------- 1 | import i18next from 'i18next'; 2 | 3 | import { MenuItemBuilderFunction } from '../../types/electron.js'; 4 | 5 | const builder: MenuItemBuilderFunction = options => { 6 | const { isMac } = options; 7 | return { 8 | label: i18next.t('MENU_EDIT'), 9 | submenu: [ 10 | { label: i18next.t('MENU_EDIT_UNDO'), role: 'undo' }, 11 | { label: i18next.t('MENU_EDIT_REDO'), role: 'redo' }, 12 | { type: 'separator' }, 13 | { label: i18next.t('MENU_EDIT_CUT'), role: 'cut' }, 14 | { label: i18next.t('MENU_EDIT_COPY'), role: 'copy' }, 15 | { label: i18next.t('MENU_EDIT_PASTE'), role: 'paste' }, 16 | { label: i18next.t('MENU_EDIT_DELETE'), role: 'delete' }, 17 | { label: i18next.t('MENU_EDIT_SELECT_ALL'), role: 'selectAll' }, 18 | isMac ? { type: 'separator' } : null, 19 | isMac 20 | ? { 21 | label: i18next.t('MENU_EDIT_SPEECH'), 22 | submenu: [ 23 | { label: i18next.t('MENU_EDIT_STARTSPEECH'), role: 'startSpeaking' }, 24 | { label: i18next.t('MENU_EDIT_STOPSPEECH'), role: 'stopSpeaking' }, 25 | ], 26 | } 27 | : null, 28 | ], 29 | }; 30 | }; 31 | 32 | export default builder; 33 | -------------------------------------------------------------------------------- /kmfrontend/src/frontend/components/generic/Switch.tsx: -------------------------------------------------------------------------------- 1 | import './Switch.scss'; 2 | 3 | import { useRef } from 'react'; 4 | 5 | interface IProps { 6 | nameCommand?: string; 7 | isChecked: boolean | undefined; 8 | handleChange?: (e: any) => void; 9 | idInput?: string; 10 | disabled?: boolean; 11 | onLabel?: string; 12 | offLabel?: string; 13 | } 14 | 15 | function Switch(props: IProps) { 16 | const checkbox = useRef(); 17 | 18 | const onKeyDown = e => { 19 | e.preventDefault(); 20 | checkbox.current.click(); 21 | }; 22 | 23 | return ( 24 | 42 | ); 43 | } 44 | 45 | export default Switch; 46 | -------------------------------------------------------------------------------- /src/dao/karafile.ts: -------------------------------------------------------------------------------- 1 | import { formatKaraV4 } from '../lib/dao/karafile.js'; 2 | import { DBKara } from '../lib/types/database/kara.js'; 3 | import logger from '../lib/utils/logger.js'; 4 | import Task from '../lib/utils/taskManager.js'; 5 | import { editKara } from '../services/karaCreation.js'; 6 | 7 | const service = 'DBKara'; 8 | 9 | export async function removeParentInKaras(kid: string, karasWithParent: DBKara[]) { 10 | if (karasWithParent.length === 0) return; 11 | logger.info(`Removing parent ${kid} in kara files`, { service }); 12 | const task = new Task({ 13 | text: 'DELETING_PARENT_IN_PROGRESS', 14 | }); 15 | try { 16 | logger.info(`Removing in ${karasWithParent.length} files`, { service }); 17 | for (const kara of karasWithParent) { 18 | logger.info(`Removing in ${kara.karafile}...`, { service }); 19 | if (kara.parents) { 20 | kara.parents = kara.parents.filter(p => p !== kid); 21 | if (kara.parents.length === 0) kara.parents = undefined; 22 | kara.modified_at = new Date(); 23 | await editKara({ 24 | kara: formatKaraV4(kara), 25 | }); 26 | } 27 | } 28 | } catch (err) { 29 | throw err; 30 | } finally { 31 | task.end(); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | // Target latest version of ECMAScript. 4 | "target": "ESNext", 5 | "module": "NodeNext", 6 | // Search under node_modules for non-relative imports. 7 | "moduleResolution": "NodeNext", 8 | // Process & infer types from .js files. 9 | "allowJs": true, 10 | // Don't emit; allow esbuild to transform files. 11 | "noEmit": true, 12 | // Enable strictest settings like strictNullChecks & noImplicitAny. 13 | // true is better but requires a global refactoring. 14 | "strict": false, 15 | "isolatedModules": true, 16 | // Import non-ES modules as default imports. 17 | "esModuleInterop": true, 18 | // Import constants from JSON (locales). 19 | "resolveJsonModule": true, 20 | "sourceMap": true, 21 | "noEmitHelpers": true, 22 | "importHelpers": true, 23 | "inlineSources": true, 24 | "pretty": true, 25 | "removeComments": true, 26 | "downlevelIteration": true, 27 | "listFiles": false, 28 | "listEmittedFiles": false, 29 | "noUnusedParameters": true, 30 | "noUnusedLocals": true, 31 | "experimentalDecorators": true, 32 | "skipLibCheck": true 33 | }, 34 | "include": ["src", "test", "util", "testUnit"] 35 | } 36 | -------------------------------------------------------------------------------- /util/esbuild.js: -------------------------------------------------------------------------------- 1 | import electron from 'electron'; 2 | import { build, buildSync } from 'esbuild'; 3 | import { execa } from 'execa'; 4 | import { rimraf } from 'rimraf'; 5 | 6 | const buildOptions = { 7 | outfile: 'dist/index.cjs', 8 | entryPoints: ['src/index.ts'], 9 | platform: 'node', 10 | target: 'node20', 11 | format: 'cjs', 12 | bundle: true, 13 | sourcemap: true, 14 | conditions: ['module'], 15 | external: ['electron', 'pg-native', 'fsevents'], 16 | legalComments: 'external', 17 | color: true, 18 | logLevel: 'info', 19 | }; 20 | 21 | let edited = true; 22 | 23 | console.log('Clearing dist/'); 24 | await rimraf('dist/'); 25 | 26 | if (process.argv[2] === 'watch') { 27 | console.log('Launching esbuild'); 28 | const builder = await build({ 29 | ...buildOptions, 30 | watch: { 31 | onRebuild: err => { 32 | edited = !err; 33 | }, 34 | }, 35 | minify: false, 36 | }); 37 | console.log('Electron watch, close the app to rerun after edits, close without edits to quit'); 38 | while (edited) { 39 | edited = false; 40 | await execa(electron, ['.'], { stdio: 'inherit' }); 41 | } 42 | builder.stop(); 43 | } else { 44 | buildSync(buildOptions); 45 | } 46 | -------------------------------------------------------------------------------- /src/controllers/frontend/importBase.ts: -------------------------------------------------------------------------------- 1 | import { Socket } from 'socket.io'; 2 | 3 | import { WS_CMD } from '../../../kmfrontend/src/utils/ws.js'; 4 | import { APIMessage } from '../../lib/services/frontend.js'; 5 | import { APIData } from '../../lib/types/api.js'; 6 | import { SocketIOApp } from '../../lib/utils/ws.js'; 7 | import { findFilesToImport, importBase } from '../../services/importBase.js'; 8 | import { runChecklist } from '../middlewares.js'; 9 | 10 | export default function importBaseController(router: SocketIOApp) { 11 | router.route(WS_CMD.FIND_FILES_TO_IMPORT, async (socket: Socket, req: APIData) => { 12 | await runChecklist(socket, req, 'admin', 'open'); 13 | try { 14 | return await findFilesToImport(req.body.dirname, req.body.template, true); 15 | } catch (err) { 16 | throw { code: err.code || 500, message: APIMessage(err.message) }; 17 | } 18 | }); 19 | router.route(WS_CMD.IMPORT_BASE, async (socket: Socket, req: APIData) => { 20 | await runChecklist(socket, req, 'admin', 'open'); 21 | try { 22 | importBase(req.body.source, req.body.template, req.body.repoDest); 23 | } catch (err) { 24 | throw { code: err.code || 500, message: APIMessage(err.message) }; 25 | } 26 | }); 27 | } 28 | -------------------------------------------------------------------------------- /kmfrontend/src/store/useGlobalState.ts: -------------------------------------------------------------------------------- 1 | import { useReducer } from 'react'; 2 | 3 | import AuthReducer, { initialStateAuth } from './reducers/auth'; 4 | import FrontendContextReducer from './reducers/frontendContext'; 5 | import ModalReducer from './reducers/modal'; 6 | import SettingsReducer, { initialStateConfig } from './reducers/settings'; 7 | 8 | // combine reducers ala Redux: each can handle its own slice 9 | const combineReducers = slices => (prevState, action) => 10 | // I like to use array.reduce, you can also just write a for..in loop 11 | Object.keys(slices).reduce( 12 | (nextState, nextProp) => ({ 13 | ...nextState, 14 | [nextProp]: slices[nextProp](prevState[nextProp], action), 15 | }), 16 | prevState 17 | ); 18 | 19 | const useGlobalState = () => { 20 | const [globalState, globalDispatch] = useReducer( 21 | combineReducers({ 22 | auth: AuthReducer, 23 | frontendContext: FrontendContextReducer, 24 | modal: ModalReducer, 25 | settings: SettingsReducer, 26 | }), 27 | { 28 | auth: initialStateAuth, 29 | frontendContext: {}, 30 | modal: {}, 31 | settings: initialStateConfig, 32 | } 33 | ); 34 | return { globalState, globalDispatch }; 35 | }; 36 | 37 | export default useGlobalState; 38 | -------------------------------------------------------------------------------- /kmfrontend/src/frontend/components/generic/buttons/AddKaraButton.tsx: -------------------------------------------------------------------------------- 1 | import i18next from 'i18next'; 2 | import { useContext } from 'react'; 3 | 4 | import { DBKara } from '../../../../../../src/lib/types/database/kara'; 5 | import GlobalContext from '../../../../store/context'; 6 | import { commandBackend } from '../../../../utils/socket'; 7 | import { PLCCallback } from '../../../../utils/tools'; 8 | import { WS_CMD } from '../../../../utils/ws'; 9 | 10 | interface Props { 11 | kara: DBKara; 12 | scope: 'admin' | 'public'; 13 | } 14 | 15 | export default function AddKaraButton(props: Props) { 16 | const context = useContext(GlobalContext); 17 | 18 | const addKara = async () => { 19 | let response; 20 | try { 21 | response = await commandBackend(WS_CMD.ADD_KARA_TO_PUBLIC_PLAYLIST, { 22 | requestedby: context.globalState.auth.data.username, 23 | kids: [props.kara.kid], 24 | }); 25 | } catch (_) { 26 | // already display 27 | } 28 | PLCCallback(response, context, props.kara, props.scope); 29 | }; 30 | 31 | return ( 32 | 36 | ); 37 | } 38 | -------------------------------------------------------------------------------- /kmfrontend/src/store/reducers/auth.ts: -------------------------------------------------------------------------------- 1 | import { AuthAction, AuthStore, LoginFailure, LoginSuccess, LogoutUser } from '../types/auth'; 2 | 3 | export const initialStateAuth: AuthStore = { 4 | data: { 5 | token: '', 6 | onlineToken: '', 7 | role: '', 8 | username: '', 9 | }, 10 | isAuthenticated: false, 11 | error: '', 12 | }; 13 | 14 | export default function authReducer(state, action: LoginSuccess | LoginFailure | LogoutUser) { 15 | switch (action.type) { 16 | case AuthAction.LOGIN_SUCCESS: 17 | return { 18 | ...state, 19 | isAuthenticated: true, 20 | data: { 21 | ...action.payload, 22 | }, 23 | error: '', 24 | }; 25 | case AuthAction.LOGIN_FAILURE: 26 | return { 27 | ...state, 28 | isAuthenticated: false, 29 | data: { 30 | token: '', 31 | onlineToken: '', 32 | role: '', 33 | username: '', 34 | }, 35 | error: action.payload.error, 36 | }; 37 | case AuthAction.LOGOUT_USER: 38 | return { 39 | ...state, 40 | isAuthenticated: false, 41 | data: { 42 | token: '', 43 | onlineToken: '', 44 | role: '', 45 | username: '', 46 | }, 47 | error: '', 48 | }; 49 | default: 50 | return state; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /util/changeUserPassword.js: -------------------------------------------------------------------------------- 1 | // This script changes a user's password 2 | 3 | import bcrypt from 'bcryptjs'; 4 | import { readFileSync } from 'fs'; 5 | import { load } from 'js-yaml'; 6 | import pg from 'pg'; 7 | 8 | async function hashPassword(password) { 9 | return bcrypt.hash(password, 10); 10 | } 11 | 12 | async function main() { 13 | const configFile = readFileSync('app/config.yml', 'utf-8'); 14 | const config = load(configFile); 15 | const dbConfig = { 16 | host: config.System.Database.host, 17 | user: config.System.Database.username, 18 | port: config.System.Database.port, 19 | password: config.System.Database.password, 20 | database: config.System.Database.database, 21 | }; 22 | const client = new pg.Pool(dbConfig); 23 | const user = process.argv[2]; 24 | const password = await hashPassword(process.argv[3]); 25 | try { 26 | await client.connect(); 27 | await client.query(` 28 | UPDATE users SET password = '${password}' WHERE pk_login = '${user}'; 29 | `); 30 | } catch (err) { 31 | // Do nothing here. 32 | } 33 | } 34 | 35 | main() 36 | .then(() => { 37 | console.log('User modified'); 38 | process.exit(0); 39 | }) 40 | .catch(err => { 41 | console.log(err); 42 | process.exit(1); 43 | }); 44 | -------------------------------------------------------------------------------- /kmfrontend/src/systempanel/pages/Karas/KaraNew.tsx: -------------------------------------------------------------------------------- 1 | import { Layout } from 'antd'; 2 | import i18next from 'i18next'; 3 | import { useNavigate } from 'react-router-dom'; 4 | 5 | import { addListener, removeListener } from '../../../utils/electron'; 6 | import { commandBackend } from '../../../utils/socket'; 7 | import Title from '../../components/Title'; 8 | import KaraForm from './KaraForm'; 9 | import { useEffect } from 'react'; 10 | import type { EditedKara } from '../../../../../src/lib/types/kara'; 11 | import { WS_CMD } from '../../../utils/ws'; 12 | 13 | function KaraNew() { 14 | const navigate = useNavigate(); 15 | 16 | const saveNew = async (kara: EditedKara) => { 17 | try { 18 | await commandBackend(WS_CMD.CREATE_KARA, kara, true, 300000); 19 | addListener(); 20 | navigate('/system/karas'); 21 | } catch (_) { 22 | // already display 23 | } 24 | }; 25 | 26 | useEffect(() => { 27 | removeListener(); 28 | }, []); 29 | 30 | return ( 31 | <> 32 | 36 | <Layout.Content> 37 | <KaraForm save={saveNew} /> 38 | </Layout.Content> 39 | </> 40 | ); 41 | } 42 | 43 | export default KaraNew; 44 | -------------------------------------------------------------------------------- /src/dao/sql/download.ts: -------------------------------------------------------------------------------- 1 | export const sqlselectDownloads = (pending: boolean) => ` 2 | SELECT name, 3 | size, 4 | status, 5 | pk_uuid as uuid, 6 | started_at, 7 | repository, 8 | mediafile, 9 | fk_kid AS kid 10 | FROM download 11 | ${pending ? "WHERE status = 'DL_PLANNED'" : ''} 12 | ORDER BY started_at DESC 13 | `; 14 | 15 | export const sqlupdateRunningDownloads = ` 16 | UPDATE download 17 | SET status = 'DL_PLANNED' 18 | WHERE status = 'DL_RUNNING' 19 | `; 20 | 21 | export const sqldeleteDoneFailedDownloads = ` 22 | DELETE FROM download 23 | WHERE status = 'DL_DONE' OR status = 'DL_FAILED' 24 | `; 25 | 26 | export const sqlsetDownloaded = ` 27 | UPDATE kara SET download_status = $1 28 | `; 29 | 30 | export const sqlsetDownloadedAK = ` 31 | UPDATE all_karas SET download_status = $1 32 | `; 33 | 34 | export const sqlinsertDownload = ` 35 | INSERT INTO download( 36 | name, 37 | size, 38 | status, 39 | pk_uuid, 40 | repository, 41 | mediafile, 42 | fk_kid 43 | ) VALUES( 44 | $1, 45 | $2, 46 | $3, 47 | $4, 48 | $5, 49 | $6, 50 | $7) 51 | `; 52 | 53 | export const sqlupdateDownloadStatus = ` 54 | UPDATE download 55 | SET status = $1 56 | WHERE pk_uuid = $2 57 | `; 58 | 59 | export const sqlemptyDownload = 'TRUNCATE download CASCADE'; 60 | -------------------------------------------------------------------------------- /kmfrontend/eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import js from '@eslint/js'; 2 | import ts from 'typescript-eslint'; 3 | import react from 'eslint-plugin-react'; 4 | import security from 'eslint-plugin-security'; 5 | import prettierConfig from 'eslint-config-prettier'; 6 | import globals from 'globals'; 7 | 8 | export default ts.config( 9 | js.configs.recommended, 10 | ...ts.configs.recommended, 11 | react.configs.flat.recommended, 12 | security.configs.recommended, 13 | prettierConfig, 14 | { 15 | files: ['**/*.ts', '**/*.tsx', '**/*.mts'], 16 | ignores: ['dist/**'], 17 | languageOptions: { 18 | globals: { 19 | ...globals.browser, 20 | ...globals.es2020, 21 | }, 22 | parser: ts.parser, 23 | parserOptions: { 24 | tsconfigRootDir: __dirname, 25 | }, 26 | }, 27 | rules: { 28 | 'react/react-in-jsx-scope': 'off', 29 | '@typescript-eslint/no-unused-vars': [ 30 | 'error', 31 | { 32 | args: 'all', 33 | argsIgnorePattern: '^_', 34 | caughtErrors: 'all', 35 | caughtErrorsIgnorePattern: '^_', 36 | destructuredArrayIgnorePattern: '^_', 37 | varsIgnorePattern: '^_', 38 | ignoreRestSiblings: true, 39 | }, 40 | ], 41 | 'security/detect-object-injection': 'off', 42 | }, 43 | settings: { 44 | react: { 45 | version: 'detect', 46 | }, 47 | }, 48 | } 49 | ); 50 | -------------------------------------------------------------------------------- /kmfrontend/src/frontend/components/generic/buttons/UpvoteKaraButton.tsx: -------------------------------------------------------------------------------- 1 | import i18next from 'i18next'; 2 | 3 | import { DBKara } from '../../../../../../src/lib/types/database/kara'; 4 | import { DBPLCInfo } from '../../../../../../src/types/database/playlist'; 5 | import { commandBackend } from '../../../../utils/socket'; 6 | import { WS_CMD } from '../../../../utils/ws'; 7 | 8 | interface Props { 9 | kara: DBKara | DBPLCInfo; 10 | wide?: boolean; 11 | updateKara?: () => void; 12 | } 13 | 14 | export default function UpvoteKaraButton(props: Props) { 15 | const upvoteKara = e => { 16 | e.stopPropagation(); 17 | const plc_id = props.kara.public_plc_id[0]; 18 | const data = props.kara.flag_upvoted ? { downvote: true, plc_id: plc_id } : { plc_id: plc_id }; 19 | commandBackend(WS_CMD.VOTE_PLC, data).catch(() => {}); 20 | props.updateKara && props.updateKara(); 21 | }; 22 | 23 | return ( 24 | <button 25 | title={props.wide ? '' : i18next.t('TOOLTIP_UPVOTE')} 26 | className={`btn btn-action karaLineButton upvoteKara`} 27 | onClick={upvoteKara} 28 | disabled={props.kara.my_public_plc_id?.length > 0} 29 | > 30 | <i className={`fas fa-thumbs-up ${props.kara?.flag_upvoted ? 'currentUpvote' : ''}`} /> 31 | {props.wide ? i18next.t('TOOLTIP_UPVOTE') : ''} 32 | </button> 33 | ); 34 | } 35 | -------------------------------------------------------------------------------- /kmfrontend/src/frontend/components/modals/UsersModal.scss: -------------------------------------------------------------------------------- 1 | .userlist { 2 | width: 100%; 3 | list-style: none; 4 | 5 | .userLine { 6 | display: flex; 7 | width: 100%; 8 | justify-content: flex-start; 9 | align-items: center; 10 | height: 3em; 11 | padding: 3px; 12 | cursor: pointer; 13 | user-select: none; 14 | &:hover { 15 | background-color: #333333; 16 | } 17 | } 18 | 19 | .list-group-item { 20 | display: block; 21 | background-color: #2b2b2b; 22 | border: 1px solid #565656; 23 | border-left: 6px solid #893230; 24 | line-height: 2.5em; 25 | &.online { 26 | border-left-color: #4c8830; 27 | } 28 | } 29 | 30 | .userDetails { 31 | background-color: #212121; 32 | text-align: start; 33 | padding: 1em; 34 | margin-top: 0.25em; 35 | > div > i { 36 | padding: 1em; 37 | margin-right: 1em; 38 | } 39 | } 40 | 41 | > li.open > .userDetails { 42 | display: block; 43 | } 44 | 45 | .userLine > img, 46 | .userLine > div { 47 | vertical-align: middle; 48 | height: 100%; 49 | } 50 | 51 | .userLine > img { 52 | border-radius: 15%; 53 | } 54 | } 55 | 56 | .blur-hover { 57 | filter: blur(5px); 58 | transition: filter 1.5s ease; 59 | margin-right: 0.5em; 60 | } 61 | 62 | .blur-hover:hover { 63 | filter: unset; 64 | } 65 | -------------------------------------------------------------------------------- /src/electron/menus/options.ts: -------------------------------------------------------------------------------- 1 | import i18next from 'i18next'; 2 | 3 | import { getConfig, setConfig } from '../../lib/utils/config.js'; 4 | import { MenuItemBuilderFunction } from '../../types/electron.js'; 5 | import { getState } from '../../utils/state.js'; 6 | import { urls } from './index.js'; 7 | 8 | const builder: MenuItemBuilderFunction = options => { 9 | const { isMac, layout } = options; 10 | const isReduced = layout === 'REDUCED'; 11 | if (isReduced) { 12 | return null; 13 | } 14 | return { 15 | label: i18next.t('MENU_OPTIONS'), 16 | visible: !isMac, 17 | submenu: [ 18 | { 19 | label: i18next.t('MENU_OPTIONS_CHECKFORUPDATES'), 20 | type: 'checkbox', 21 | checked: getConfig().Online.Updates.App, 22 | visible: !getState().forceDisableAppUpdate, 23 | click: () => { 24 | setConfig({ Online: { Updates: { App: !getConfig().Online.Updates.App } } }); 25 | }, 26 | }, 27 | !isReduced ? { type: 'separator' } : null, 28 | { 29 | label: i18next.t('MENU_OPTIONS_OPERATORCONFIG'), 30 | accelerator: 'CmdOrCtrl+T', 31 | click: urls.operatorOptions, 32 | }, 33 | { 34 | label: i18next.t('MENU_OPTIONS_SYSTEMCONFIG'), 35 | accelerator: 'CmdOrCtrl+G', 36 | click: urls.systemOptions, 37 | }, 38 | ], 39 | }; 40 | }; 41 | 42 | export default builder; 43 | -------------------------------------------------------------------------------- /kmfrontend/src/frontend/components/decorators/KmAppWrapperDecorator.tsx: -------------------------------------------------------------------------------- 1 | import { ReactNode, useEffect, useState } from 'react'; 2 | 3 | interface IProps { 4 | children?: ReactNode; 5 | single?: boolean; 6 | chibi?: boolean; 7 | top?: string; 8 | bottom?: string; 9 | hmagrin?: boolean; 10 | } 11 | 12 | function KmAppWrapperDecorator(props: IProps) { 13 | const [barOffset, setBarOffset] = useState('0'); 14 | 15 | const listener = () => { 16 | const vhHeight = parseInt(window.getComputedStyle(document.getElementById('height-compute')).height); 17 | setBarOffset(`${vhHeight - Math.floor(visualViewport.height)}px`); 18 | }; 19 | 20 | useEffect(() => { 21 | listener(); 22 | visualViewport.addEventListener('resize', listener, { passive: true }); 23 | return () => { 24 | visualViewport.removeEventListener('resize', listener); 25 | }; 26 | }, []); 27 | 28 | return ( 29 | <div 30 | className={`KmAppWrapperDecorator${props.single ? ' single' : ''}${ 31 | props.hmagrin !== false ? ' hmargin' : '' 32 | }${props.chibi ? ' chibi' : ''}`} 33 | style={{ 34 | ['--top' as any]: props.top, 35 | ['--bar-offset' as any]: barOffset, 36 | ['--bottom' as any]: props.bottom, 37 | }} 38 | > 39 | {props.children} 40 | </div> 41 | ); 42 | } 43 | 44 | export default KmAppWrapperDecorator; 45 | -------------------------------------------------------------------------------- /kmfrontend/src/frontend/styles/components/tags.scss: -------------------------------------------------------------------------------- 1 | @use 'sass:color'; 2 | @use '../variables'; 3 | 4 | .tagConteneur { 5 | display: flex; 6 | align-items: flex-start; 7 | flex-wrap: wrap; 8 | @media screen and (max-width: variables.$mugen-breakpoint-large) { 9 | margin: 0; 10 | flex-wrap: wrap; 11 | .tag { 12 | margin-top: 0.4em; 13 | font-size: 0.9em; 14 | padding: 0.125em 0.25em !important; 15 | } 16 | } 17 | .tag { 18 | margin-right: 0.4em; 19 | margin-bottom: 0.2em; 20 | float: right; 21 | } 22 | } 23 | 24 | .tag { 25 | background-color: variables.$mugen-tag-default; 26 | @each $name, $color in variables.$mugen-colors { 27 | @if $name == 'white' { 28 | &.#{$name} { 29 | background-color: $color; 30 | color: black; 31 | } 32 | } @else { 33 | &.#{$name} { 34 | background-color: color.scale($color, $lightness: -10%, $saturation: -40%); 35 | color: white; 36 | } 37 | } 38 | } 39 | &.problematicTag, 40 | .problematicTag { 41 | color: gold; 42 | } 43 | &.inline { 44 | margin: 0 0.5em; 45 | } 46 | padding: 0.25em 0.75em; 47 | text-align: center; 48 | font-weight: bold; 49 | color: #aaa; 50 | box-shadow: 51 | 0 6px 8px 2px #00000005, 52 | 0 1px 5px 2px #00000004, 53 | 0 2px 3px -1px #00000005; 54 | text-shadow: none; 55 | border-radius: 5px; 56 | } 57 | -------------------------------------------------------------------------------- /src/dao/favorites.ts: -------------------------------------------------------------------------------- 1 | import { pg as yesql } from 'yesql'; 2 | 3 | import { db, transaction } from '../lib/dao/database.js'; 4 | import { KaraParams } from '../lib/types/kara.js'; 5 | import { FavoritesMicro } from '../types/favorites.js'; 6 | import { sqlclearFavorites, sqlgetFavoritesMicro, sqlinsertFavorites, sqlremoveFavorites } from './sql/favorites.js'; 7 | 8 | export async function selectFavoritesMicro(params: KaraParams): Promise<FavoritesMicro[]> { 9 | const finalParams = { username: params.username }; 10 | let limitClause = ''; 11 | let offsetClause = ''; 12 | if (params.from > 0) offsetClause = `OFFSET ${params.from} `; 13 | if (params.size > 0) limitClause = `LIMIT ${params.size} `; 14 | const query = sqlgetFavoritesMicro(limitClause, offsetClause); 15 | const res = await db().query(yesql(query)(finalParams)); 16 | return res.rows; 17 | } 18 | 19 | export function deleteFavorites(fList: string[], username: string) { 20 | const karas = fList.map(kara => [kara, username]); 21 | return transaction({ params: karas, sql: sqlremoveFavorites }); 22 | } 23 | 24 | export function truncateFavorites(username: string) { 25 | return db().query(sqlclearFavorites, [username]); 26 | } 27 | 28 | export function insertFavorites(karaList: string[], username: string) { 29 | const karas = karaList.map(kara => [kara, username]); 30 | return transaction({ params: karas, sql: sqlinsertFavorites }); 31 | } 32 | -------------------------------------------------------------------------------- /src/controllers/frontend/backgrounds.ts: -------------------------------------------------------------------------------- 1 | import { WS_CMD } from '../../../kmfrontend/src/utils/ws.js'; 2 | import { APIMessage } from '../../lib/services/frontend.js'; 3 | import { SocketIOApp } from '../../lib/utils/ws.js'; 4 | import { addBackgroundFile, getBackgroundFiles, removeBackgroundFile } from '../../services/backgrounds.js'; 5 | import { runChecklist } from '../middlewares.js'; 6 | 7 | export default function backgroundsController(router: SocketIOApp) { 8 | router.route(WS_CMD.GET_BACKGROUND_FILES, async (socket, req) => { 9 | await runChecklist(socket, req, 'admin', 'open'); 10 | try { 11 | return await getBackgroundFiles(req.body.type); 12 | } catch (err) { 13 | throw { code: err.code || 500, message: APIMessage(err.message) }; 14 | } 15 | }); 16 | router.route(WS_CMD.ADD_BACKGROUND, async (socket, req) => { 17 | await runChecklist(socket, req, 'admin', 'open'); 18 | try { 19 | return await addBackgroundFile(req.body.type, req.body.file); 20 | } catch (err) { 21 | throw { code: err.code || 500, message: APIMessage(err.message) }; 22 | } 23 | }); 24 | 25 | router.route(WS_CMD.REMOVE_BACKGROUND, async (socket, req) => { 26 | await runChecklist(socket, req, 'admin', 'open'); 27 | try { 28 | return await removeBackgroundFile(req.body.type, req.body.file); 29 | } catch (err) { 30 | throw { code: err.code || 500, message: APIMessage(err.message) }; 31 | } 32 | }); 33 | } 34 | -------------------------------------------------------------------------------- /src/electron/electronMenu.ts: -------------------------------------------------------------------------------- 1 | import { Menu } from 'electron'; 2 | 3 | import { removeNulls } from '../lib/utils/objectHelpers.js'; 4 | import { MenuItemBuilderOptions, MenuLayout } from '../types/electron.js'; 5 | import { win } from './electron.js'; 6 | import editMenu from './menus/edit.js'; 7 | import fileMenu from './menus/file.js'; 8 | import goToMenu from './menus/goTo.js'; 9 | import helpMenu from './menus/help.js'; 10 | import optionsMenu from './menus/options.js'; 11 | import toolsMenu from './menus/tools.js'; 12 | import viewMenu from './menus/view.js'; 13 | import windowMenu from './menus/window.js'; 14 | 15 | function initMenu(layout: MenuLayout) { 16 | const options: MenuItemBuilderOptions = { 17 | isMac: process.platform === 'darwin', 18 | layout, 19 | }; 20 | return removeNulls([ 21 | // MAIN MENU / FILE MENU 22 | fileMenu(options), 23 | // VIEW MENU 24 | viewMenu(options), 25 | // EDIT MENU 26 | editMenu(options), 27 | // GO TO MENU 28 | goToMenu(options), 29 | // TOOLS MENU 30 | toolsMenu(options), 31 | // OPTIONS 32 | optionsMenu(options), 33 | // WINDOW MENU 34 | windowMenu(options), 35 | // HELP MENU 36 | helpMenu(options), 37 | ]); 38 | } 39 | 40 | export function createMenu(layout: MenuLayout) { 41 | const menu = Menu.buildFromTemplate(initMenu(layout)); 42 | process.platform === 'darwin' ? Menu.setApplicationMenu(menu) : win?.setMenu(menu); 43 | } 44 | -------------------------------------------------------------------------------- /kmfrontend/src/store/reducers/frontendContext.ts: -------------------------------------------------------------------------------- 1 | import { FrontendContextAction, FrontendContextStore } from '../types/frontendContext'; 2 | 3 | export default function frontendContextReducer(state: FrontendContextStore, action): FrontendContextStore { 4 | switch (action.type) { 5 | case FrontendContextAction.FILTER_VALUE: 6 | if ( 7 | (action.payload.side === 'left' && action.payload.filterValue !== state.filterValue1) || 8 | (action.payload.side === 'right' && action.payload.filterValue !== state.filterValue2) 9 | ) { 10 | if (action.payload.side === 'left') { 11 | return { ...state, filterValue1: action.payload.filterValue }; 12 | } else { 13 | return { ...state, filterValue2: action.payload.filterValue }; 14 | } 15 | } else { 16 | return state; 17 | } 18 | case FrontendContextAction.BG_IMAGE: 19 | return { ...state, backgroundImg: action.payload.backgroundImg }; 20 | case FrontendContextAction.PLAYLIST_INFO_LEFT: 21 | return { ...state, playlistInfoLeft: action.payload.playlist }; 22 | case FrontendContextAction.PLAYLIST_INFO_RIGHT: 23 | return { ...state, playlistInfoRight: action.payload.playlist }; 24 | case FrontendContextAction.INDEX_KARA_DETAIL: 25 | return { ...state, indexKaraDetail: action.payload.indexKaraDetail }; 26 | case FrontendContextAction.FUTURE_TIME: 27 | return { ...state, futurTime: action.payload.futurTime }; 28 | default: 29 | return state; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /kmfrontend/src/frontend/components/public/LyricsBox.scss: -------------------------------------------------------------------------------- 1 | @use 'sass:color'; 2 | @use '../../styles/variables'; 3 | 4 | .lyrics-box { 5 | &.mobile { 6 | @media screen and (min-width: variables.$mugen-breakpoint-large) { 7 | display: none; 8 | } 9 | > .lyrics { 10 | margin: 1em auto 2em; 11 | } 12 | } 13 | > .toggle { 14 | text-align: center; 15 | width: 100%; 16 | cursor: pointer; 17 | user-select: none; 18 | font-size: 1.125em; 19 | padding: 1em; 20 | } 21 | > .lyrics { 22 | width: 100%; 23 | background-color: color.adjust(variables.$mugen-background, $lightness: 10%, $space: hsl); 24 | padding: 1em; 25 | margin: 0 0.5em; 26 | border-radius: 15px; 27 | text-align: center; 28 | > div { 29 | transition: 30 | font-size, 31 | color, 32 | margin ease 200ms, 33 | 200ms, 34 | 200ms; 35 | span { 36 | &.singing { 37 | color: color.scale(skyblue, $saturation: 100%, $lightness: -15%); 38 | } 39 | } 40 | &.hidden { 41 | font-size: 0; 42 | } 43 | &.greyed { 44 | color: color.adjust(variables.$mugen-background, $lightness: 65%, $space: hsl); 45 | } 46 | &.current { 47 | font-size: 1.25em; 48 | font-weight: bold; 49 | color: ghostwhite; 50 | margin: 0.25em 0; 51 | &.forced { 52 | color: goldenrod; 53 | } 54 | } 55 | &.incoming { 56 | color: white; 57 | margin: 0.1em 0; 58 | } 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /migrations/20190522101040.do.addSessionTableAndColumns.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE session ( 2 | pk_seid UUID, 3 | name CHARACTER VARYING, 4 | started_at TIMESTAMPTZ UNIQUE NOT NULL 5 | ); 6 | 7 | INSERT INTO session (started_at) 8 | SELECT DISTINCT session_started_at FROM played; 9 | 10 | INSERT INTO session (started_at) 11 | SELECT DISTINCT session_started_at FROM requested 12 | ON CONFLICT DO NOTHING; 13 | 14 | UPDATE session SET name = started_at; 15 | 16 | UPDATE session SET pk_seid = (SELECT uuid_in(overlay(overlay(md5(random()::text || ':' || started_at::text) placing '4' from 13) placing to_hex(floor(random()*(11-8+1) + 8)::int)::text from 17)::cstring)); 17 | 18 | ALTER TABLE session ADD PRIMARY KEY(pk_seid); 19 | 20 | ALTER TABLE played ADD COLUMN fk_seid UUID; 21 | ALTER TABLE requested ADD COLUMN fk_seid UUID; 22 | ALTER TABLE played ADD CONSTRAINT played_fk_seid_fkey FOREIGN KEY (fk_seid) REFERENCES session (pk_seid) ON DELETE CASCADE; 23 | ALTER TABLE requested ADD CONSTRAINT requested_fk_seid_fkey FOREIGN KEY (fk_seid) REFERENCES session (pk_seid) ON DELETE CASCADE; 24 | 25 | UPDATE played SET fk_seid = (SELECT pk_seid FROM session WHERE started_at = session_started_at); 26 | UPDATE requested SET fk_seid = (SELECT pk_seid FROM session WHERE started_at = session_started_at); 27 | 28 | ALTER TABLE played DROP COLUMN session_started_at; 29 | ALTER TABLE requested DROP COLUMN session_started_at; 30 | 31 | ALTER TABLE session DROP CONSTRAINT session_started_at_key; -------------------------------------------------------------------------------- /src/dao/tagfile.ts: -------------------------------------------------------------------------------- 1 | import { formatKaraV4 } from '../lib/dao/karafile.js'; 2 | import { DBKara } from '../lib/types/database/kara.js'; 3 | import { DBTag } from '../lib/types/database/tag.js'; 4 | import { tagTypes } from '../lib/utils/constants.js'; 5 | import logger from '../lib/utils/logger.js'; 6 | import Task from '../lib/utils/taskManager.js'; 7 | import { editKara } from '../services/karaCreation.js'; 8 | 9 | const service = 'DBTag'; 10 | 11 | export async function removeTagInKaras(tag: DBTag, karasWithTag: DBKara[]) { 12 | if (karasWithTag.length === 0) return; 13 | logger.info(`Removing tag ${tag.tid} in kara files`, { service }); 14 | const task = new Task({ 15 | text: 'DELETING_TAG_IN_PROGRESS', 16 | subtext: tag.name, 17 | }); 18 | try { 19 | logger.info(`Removing in ${karasWithTag.length} files`, { service }); 20 | for (const karaWithTag of karasWithTag) { 21 | logger.info(`Removing in ${karaWithTag.karafile}...`, { service }); 22 | for (const type of Object.keys(tagTypes)) { 23 | if (karaWithTag[type]) karaWithTag[type] = karaWithTag[type].filter((t: DBTag) => t.tid !== tag.tid); 24 | } 25 | karaWithTag.modified_at = new Date(); 26 | // We don't enable refresh on these edits because that'll be done later. 27 | await editKara( 28 | { 29 | kara: formatKaraV4(karaWithTag), 30 | }, 31 | false 32 | ); 33 | } 34 | } catch (err) { 35 | throw err; 36 | } finally { 37 | task.end(); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /kmfrontend/src/frontend/KMFrontend.scss: -------------------------------------------------------------------------------- 1 | // Minireset 2 | @use 'minireset.css/minireset'; 3 | @use 'styles/variables'; 4 | 5 | // Our stylesheet 6 | @use 'styles/components/buttons'; 7 | @use 'styles/components/dropdowns'; 8 | @use 'styles/components/decorators'; 9 | @use 'styles/components/details'; 10 | @use 'styles/components/settings'; 11 | @use 'styles/components/tags'; 12 | @use 'styles/components/input'; 13 | @use 'styles/components/loader'; 14 | @use 'styles/components/titles'; 15 | 16 | // React Toastify 17 | @use '../toast'; 18 | 19 | // Fonts 20 | @use 'styles/fonts/Lato/stylesheet'; 21 | 22 | @use 'styles/al-icons/css/al-icons.css'; 23 | 24 | // FontAwesome 25 | @use '@fortawesome/fontawesome-free/css/all.min.css'; 26 | 27 | // Virtual element to be probed by KmAppWrapperDecorator to workaround URL bar resizing 28 | // https://developers.google.com/web/updates/2016/12/url-bar-resizing 29 | #height-compute { 30 | position: absolute; 31 | height: 100vh; 32 | width: 0; 33 | display: none; 34 | } 35 | 36 | body { 37 | font-family: Lato, 'Helvetica Neue', Cantarell, sans-serif; 38 | 39 | // Don't set the background here 40 | // It's defined in index.html instead, so it can be easily overridden by tools like OBS 41 | // background: variables.$mugen-background; 42 | 43 | color: white; 44 | &.no-scroll { 45 | overflow: hidden; 46 | } 47 | } 48 | 49 | a { 50 | color: unset; 51 | &:visited { 52 | color: unset; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /migrations/20200808000000.do.addNoLiveDownloadFlagToTags.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE tag ADD COLUMN noLiveDownload BOOLEAN DEFAULT(false) NOT NULL; 2 | 3 | DROP MATERIALIZED VIEW all_tags; 4 | 5 | CREATE MATERIALIZED VIEW all_tags AS 6 | WITH t_count as ( 7 | select a.fk_tid, json_agg(json_build_object('type', a.type, 'count', a.c))::text AS count_per_type 8 | FROM ( 9 | SELECT fk_tid, count(fk_kid) as c, type 10 | FROM kara_tag 11 | GROUP BY fk_tid, type) as a 12 | GROUP BY a.fk_tid 13 | ) 14 | SELECT 15 | t.name AS name, 16 | t.types AS types, 17 | t.aliases AS aliases, 18 | t.i18n AS i18n, 19 | t.pk_tid AS tid, 20 | t.problematic AS problematic, 21 | t.noLiveDownload AS noLiveDownload, 22 | tag_aliases.list AS search_aliases, 23 | t.tagfile AS tagfile, 24 | t.short as short, 25 | t.repository AS repository, 26 | t.modified_at AS modified_at, 27 | count_per_type::jsonb AS karacount 28 | FROM tag t 29 | CROSS JOIN LATERAL ( 30 | SELECT string_agg(tag_aliases.elem::text, ' ') AS list 31 | FROM jsonb_array_elements_text(t.aliases) AS tag_aliases(elem) 32 | ) tag_aliases 33 | LEFT JOIN t_count on t.pk_tid = t_count.fk_tid 34 | GROUP BY t.pk_tid, tag_aliases.list, count_per_type 35 | ORDER BY name; 36 | 37 | CREATE INDEX idx_at_name ON all_tags(name); 38 | CREATE INDEX idx_at_tid ON all_tags(tid); 39 | CREATE INDEX idx_at_search_aliases ON all_tags(search_aliases); 40 | 41 | -------------------------------------------------------------------------------- /src/utils/tips.ts: -------------------------------------------------------------------------------- 1 | import i18n from 'i18next'; 2 | import { shuffle } from 'lodash'; 3 | 4 | import { TipsAndTricks, TipType } from '../types/tips.js'; 5 | 6 | let tips: TipsAndTricks; 7 | let index = 0; 8 | let activeTipType: TipType = 'normal'; 9 | 10 | export function tip() { 11 | if (!tips) initTable(); 12 | const oneTip = tips[activeTipType][index]; 13 | if (!oneTip) return { tip: '', duration: 2000, title: i18n.t(`TIPS.TITLES.${activeTipType.toUpperCase()}`) }; 14 | const words = oneTip.split(' ').length; 15 | // Calculate the estimated time for reading 16 | // Based from https://marketingland.com/estimated-reading-times-increase-engagement-79830: 17 | // The average human reads 200 words in a minute <=> 3 words per seconds 18 | // Add 2 second to let the user start reading 19 | const duration = Math.round(words / 3) * 1000 + 2000; 20 | const ret = { 21 | tip: tips[activeTipType][index], 22 | duration, 23 | title: i18n.t(`TIPS.TITLES.${activeTipType.toUpperCase()}`), 24 | }; 25 | index += 1; 26 | // Restart from the beginning if it reaches the end 27 | if (tips[activeTipType][index] === undefined) { 28 | index = 0; 29 | } 30 | return ret; 31 | } 32 | 33 | function initTable() { 34 | tips = { 35 | normal: shuffle(i18n.t('TIPS.NORMAL', { returnObjects: true })), 36 | errors: shuffle(i18n.t('TIPS.ERRORS', { returnObjects: true })), 37 | }; 38 | } 39 | 40 | export function setTipLoop(type: TipType = 'normal') { 41 | activeTipType = type; 42 | } 43 | -------------------------------------------------------------------------------- /.gitlab/sast-ruleset.toml: -------------------------------------------------------------------------------- 1 | [eslint] 2 | [[eslint.ruleset]] 3 | disable = true 4 | [eslint.ruleset.identifier] 5 | type = "eslint_rule_id" 6 | value = "security/detect-object-injection" 7 | 8 | [[eslint.ruleset]] 9 | disable = true 10 | [eslint.ruleset.identifier] 11 | type = "eslint_rule_id" 12 | value = "security/detect-non-literal-fs-filename" 13 | 14 | [[eslint.ruleset]] 15 | disable = true 16 | [eslint.ruleset.identifier] 17 | type = "eslint_rule_id" 18 | value = "security/detect-non-literal-regexp" 19 | 20 | [[eslint.ruleset]] 21 | disable = true 22 | [eslint.ruleset.identifier] 23 | type = "cwe" 24 | value = "185" 25 | 26 | [[eslint.ruleset]] 27 | disable = true 28 | [eslint.ruleset.identifier] 29 | type = "cwe" 30 | value = "94" 31 | 32 | [[eslint.ruleset]] 33 | disable = true 34 | [eslint.ruleset.identifier] 35 | type = "cwe" 36 | value = "22" 37 | 38 | [[semgrep.ruleset]] 39 | disable = true 40 | [semgrep.ruleset.identifier] 41 | type = "eslint_rule_id" 42 | value = "security/detect-object-injection" 43 | 44 | [[semgrep.ruleset]] 45 | disable = true 46 | [semgrep.ruleset.identifier] 47 | type = "eslint_rule_id" 48 | value = "security/detect-non-literal-fs-filename" 49 | 50 | [[semgrep.ruleset]] 51 | disable = true 52 | [semgrep.ruleset.identifier] 53 | type = "eslint_rule_id" 54 | value = "security/detect-non-literal-regexp" -------------------------------------------------------------------------------- /locales/ja.json: -------------------------------------------------------------------------------- 1 | { 2 | "CANCEL": "キャンセル", 3 | "YES": "はい", 4 | "NO": "いいえ", 5 | "UNKNOWN": "不明", 6 | "MENU_HELP_ABOUT": "カラオケ無限について", 7 | "MENU_HELP_DISCORD": "&Discord", 8 | "TAG_TYPES": { 9 | "SINGERGROUPS": "バンド", 10 | "SINGERS": "歌手", 11 | "WARNINGS": "警告", 12 | "SONGWRITERS": "作曲家", 13 | "LANGS": "言語", 14 | "VERSIONS": "バージョン" 15 | }, 16 | "MOVED_USER_DIR_DIALOG": { 17 | "UNDERSTOOD": "了解!" 18 | }, 19 | "TIPS": { 20 | "NORMAL": [ 21 | null, 22 | null, 23 | null, 24 | null, 25 | null, 26 | null, 27 | null, 28 | null, 29 | null, 30 | null, 31 | null, 32 | null, 33 | "お水を定期的に飲むべきですよ!" 34 | ] 35 | }, 36 | "ABOUT": { 37 | "TITLE": "カラオケ無限について" 38 | }, 39 | "TITLE": "タイトル", 40 | "LIBRARY": "ライブラリ", 41 | "MIGRATION_MESSAGES": [ 42 | null, 43 | null, 44 | null, 45 | null, 46 | null, 47 | null, 48 | null, 49 | null, 50 | null, 51 | null, 52 | null, 53 | null, 54 | "Wikipediaをダウンロード中" 55 | ], 56 | "NEXT_SONG": "次の曲へ", 57 | "MENU_FILE": "ファイル", 58 | "MENU_HELP_WEBSITE": "ウェブサイト", 59 | "MENU_HELP": "ヘルプ", 60 | "MENU_HELP_BLUESKY": "&Bluesky", 61 | "MENU_HELP_MASTODON": "&Mastodon", 62 | "MENU_HELP_GITLAB": "&GitLab", 63 | "MENU_GOTO_HOME": "ホーム", 64 | "ERROR": "エラー", 65 | "MENU_OPTIONS_OPERATORCONFIG": "設定", 66 | "MENU_OPTIONS_SYSTEMCONFIG": "システム設定", 67 | "MENU_OPTIONS_OPERATORCONFIG_OSX": "設定⋯", 68 | "MENU_OPTIONS_SYSTEMCONFIG_OSX": "システム設定⋯" 69 | } 70 | -------------------------------------------------------------------------------- /src/controllers/frontend/poll.ts: -------------------------------------------------------------------------------- 1 | import { WS_CMD } from '../../../kmfrontend/src/utils/ws.js'; 2 | import { APIMessage } from '../../lib/services/frontend.js'; 3 | import { check } from '../../lib/utils/validators.js'; 4 | import { SocketIOApp } from '../../lib/utils/ws.js'; 5 | import { addPollVote, getPoll } from '../../services/poll.js'; 6 | import { runChecklist } from '../middlewares.js'; 7 | 8 | export default function pollController(router: SocketIOApp) { 9 | router.route(WS_CMD.GET_POLL, async (socket, req) => { 10 | await runChecklist(socket, req, 'guest', 'limited'); 11 | try { 12 | return getPoll(req.token); 13 | } catch (err) { 14 | throw { code: err.code || 500, message: APIMessage(err.message) }; 15 | } 16 | }); 17 | router.route(WS_CMD.VOTE_POLL, async (socket, req) => { 18 | await runChecklist(socket, req, 'guest', 'limited'); 19 | // Validate form data 20 | const validationErrors = check(req.body, { 21 | index: { presence: true, numbersArrayValidator: true }, 22 | }); 23 | if (!validationErrors) { 24 | // No errors detected 25 | try { 26 | const ret = addPollVote(req.body.index, req.token); 27 | return { code: 200, message: APIMessage(ret.code, ret.data) }; 28 | } catch (err) { 29 | throw { code: err.code || 500, message: APIMessage(err.message) }; 30 | } 31 | } else { 32 | // Errors detected 33 | // Sending BAD REQUEST HTTP code and error object. 34 | throw { code: 400, message: validationErrors }; 35 | } 36 | }); 37 | } 38 | -------------------------------------------------------------------------------- /kmfrontend/src/utils/polyfills.ts: -------------------------------------------------------------------------------- 1 | if (!window.visualViewport) { 2 | window.visualViewport = { 3 | get offsetLeft() { 4 | return window.document.documentElement.offsetLeft; 5 | }, 6 | get offsetTop() { 7 | return window.document.documentElement.offsetTop; 8 | }, 9 | get pageLeft() { 10 | return 0; 11 | }, 12 | get pageTop() { 13 | return 0; 14 | }, 15 | get width() { 16 | return window.document.documentElement.clientWidth; 17 | }, 18 | get height() { 19 | return window.document.documentElement.clientHeight; 20 | }, 21 | get scale() { 22 | return 0; 23 | }, 24 | addEventListener: function ( 25 | type: string, 26 | listener: EventListenerOrEventListenerObject, 27 | options?: boolean | AddEventListenerOptions 28 | ) { 29 | return window.addEventListener(type, listener, options); 30 | }, 31 | removeEventListener: function ( 32 | type: string, 33 | listener: EventListenerOrEventListenerObject, 34 | options?: boolean | EventListenerOptions 35 | ) { 36 | return window.removeEventListener(type, listener, options); 37 | }, 38 | dispatchEvent: function (event: Event) { 39 | return window.dispatchEvent(event); 40 | }, 41 | get onresize() { 42 | return <any>window.onresize; 43 | }, 44 | set onresize(value) { 45 | window.onresize = value; 46 | }, 47 | get onscroll() { 48 | return <any>window.onscroll; 49 | }, 50 | set onscroll(value) { 51 | window.onscroll = value; 52 | }, 53 | }; 54 | } 55 | -------------------------------------------------------------------------------- /kmfrontend/src/frontend/styles/start/MigratePage.scss: -------------------------------------------------------------------------------- 1 | @use '../variables'; 2 | 3 | .start-page .wrapper.migrate { 4 | font-weight: 300; 5 | 6 | grid-template-columns: 200px auto; 7 | grid-template-areas: 8 | 'logo title aside' 9 | 'other main main'; 10 | 11 | @media (max-width: variables.$mugen-breakpoint-large) { 12 | grid-template-columns: auto 200px; 13 | grid-template-rows: auto auto 100%; 14 | grid-template-areas: 15 | 'aside logo' 16 | 'title logo' 17 | 'main main'; 18 | } 19 | 20 | @media (max-width: variables.$mugen-breakpoint-small) { 21 | grid-template-columns: auto; 22 | grid-template-rows: auto; 23 | grid-template-areas: 24 | 'logo' 25 | 'aside' 26 | 'title' 27 | 'main'; 28 | } 29 | 30 | .limited-width { 31 | max-width: 900px; 32 | } 33 | 34 | .justified { 35 | text-align: justify; 36 | 37 | h2 { 38 | font-size: 2.5em; 39 | margin-bottom: 0.25em; 40 | } 41 | 42 | h3 { 43 | font-size: 1.5em; 44 | margin-bottom: 0.5em; 45 | } 46 | 47 | p { 48 | margin-bottom: 1em; 49 | } 50 | 51 | ul { 52 | margin: 1em 0; 53 | list-style: circle; 54 | li { 55 | margin-bottom: 0.5em; 56 | } 57 | } 58 | } 59 | 60 | .continue-btn { 61 | cursor: pointer; 62 | color: white; 63 | border: 2px solid white; 64 | border-radius: 0.5em; 65 | padding: 0.5em 1em; 66 | text-transform: uppercase; 67 | font-weight: bold; 68 | background: none; 69 | outline: none; 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /initpage/index.html: -------------------------------------------------------------------------------- 1 | <!doctype html> 2 | <html> 3 | <head> 4 | <meta charset="UTF-8" /> 5 | <title>Karaoke Mugen 6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 |
14 |
15 | 16 |
17 |
    18 |
    19 |
    20 |
    21 |
    22 |
    23 |
    24 |
    25 |
    26 |
    27 | Nanamin 28 |
    29 |
    30 |
    31 |
    32 |
    33 |
    34 | 35 | 36 |
    37 |
    38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /kmfrontend/src/utils/components/ProfilePicture.tsx: -------------------------------------------------------------------------------- 1 | import { ImgHTMLAttributes, memo, useContext, useEffect, useState } from 'react'; 2 | 3 | import { User } from '../../../../src/lib/types/user'; 4 | import blankAvatar from '../../assets/blank.png'; 5 | import GlobalContext from '../../store/context'; 6 | import { generateProfilePicLink, syncGenerateProfilePicLink, updateCache } from '../profilePics'; 7 | 8 | interface IProps extends ImgHTMLAttributes { 9 | user: User; 10 | } 11 | 12 | function ProfilePicture(props: IProps) { 13 | const [url, setUrl] = useState(syncGenerateProfilePicLink(props.user)); 14 | const context = useContext(GlobalContext); 15 | 16 | const updateUrl = async () => { 17 | const newUrl = await generateProfilePicLink(props.user, context); 18 | setUrl(newUrl); 19 | }; 20 | 21 | useEffect(() => { 22 | if (props.user?.login) { 23 | updateUrl(); 24 | } 25 | }, []); 26 | 27 | useEffect(() => { 28 | updateUrl(); 29 | }, [props.user.avatar_file, props.user.login]); 30 | 31 | const htmlProps = { ...props, user: undefined }; 32 | return ( 33 | {props.user?.nickname} { 38 | setUrl(blankAvatar); 39 | updateCache(props.user, blankAvatar); 40 | }} 41 | {...htmlProps} 42 | /> 43 | ); 44 | } 45 | 46 | export default memo( 47 | ProfilePicture, 48 | (prev, next) => prev.user.avatar_file === next.user.avatar_file && prev.user.login === next.user.login 49 | ); 50 | -------------------------------------------------------------------------------- /kmfrontend/src/frontend/components/generic/buttons/MakeFavButton.tsx: -------------------------------------------------------------------------------- 1 | import i18next from 'i18next'; 2 | import { useCallback, useContext, useMemo } from 'react'; 3 | 4 | import GlobalContext from '../../../../store/context'; 5 | import { commandBackend } from '../../../../utils/socket'; 6 | import { displayMessage } from '../../../../utils/tools'; 7 | import { WS_CMD } from '../../../../utils/ws'; 8 | 9 | interface Props { 10 | kid: string; 11 | } 12 | 13 | export default function MakeFavButton(props: Props) { 14 | const context = useContext(GlobalContext); 15 | const isFavorite = useMemo(() => { 16 | return context.globalState.settings.data.favorites.has(props.kid); 17 | }, [context.globalState.settings.data.favorites, props.kid]); 18 | const makeFavorite = useCallback(() => { 19 | if (context.globalState.auth.data.onlineAvailable !== false) { 20 | isFavorite 21 | ? commandBackend(WS_CMD.DELETE_FAVORITES, { 22 | kids: [props.kid], 23 | }) 24 | : commandBackend(WS_CMD.ADD_FAVORITES, { 25 | kids: [props.kid], 26 | }); 27 | } else { 28 | displayMessage('warning', i18next.t('ERROR_CODES.FAVORITES_ONLINE_NOINTERNET'), 5000); 29 | return; 30 | } 31 | }, [context.globalState.auth.data.onlineAvailable, isFavorite, props.kid]); 32 | 33 | return ( 34 | 42 | ); 43 | } 44 | -------------------------------------------------------------------------------- /src/dao/session.ts: -------------------------------------------------------------------------------- 1 | import { db } from '../lib/dao/database.js'; 2 | import { Session } from '../types/session.js'; 3 | import { 4 | sqlAutoFillSessionEndedAt, 5 | sqlcleanSessions, 6 | sqldeleteSession, 7 | sqlinsertSession, 8 | sqlreplacePlayed, 9 | sqlreplaceRequested, 10 | sqlselectSessions, 11 | sqlupdateSession, 12 | } from './sql/session.js'; 13 | 14 | export async function selectSessions(): Promise { 15 | const sessions = await db().query(sqlselectSessions); 16 | return sessions.rows; 17 | } 18 | 19 | export function replaceSession(seid1: string, seid2: string) { 20 | return Promise.all([db().query(sqlreplacePlayed, [seid1, seid2]), db().query(sqlreplaceRequested, [seid1, seid2])]); 21 | } 22 | export function insertSession(session: Session) { 23 | return db().query(sqlinsertSession, [ 24 | session.seid, 25 | session.name, 26 | session.started_at, 27 | session.ended_at, 28 | session.private, 29 | ]); 30 | } 31 | 32 | export function deleteSession(seid: string) { 33 | return db().query(sqldeleteSession, [seid]); 34 | } 35 | 36 | export function updateSession(session: Session) { 37 | return db().query(sqlupdateSession, [ 38 | session.seid, 39 | session.name, 40 | session.started_at, 41 | session.ended_at, 42 | session.private, 43 | ]); 44 | } 45 | 46 | export function autoFillSessionEndedAt(seid: string) { 47 | // This autofills ALL sessions' ended_at fields if they are null, except the current session 48 | return db().query(sqlAutoFillSessionEndedAt, [seid]); 49 | } 50 | 51 | export function cleanSessions() { 52 | return db().query(sqlcleanSessions); 53 | } 54 | -------------------------------------------------------------------------------- /kmfrontend/src/utils/i18n.ts: -------------------------------------------------------------------------------- 1 | import i18n from 'i18next'; 2 | import { initReactI18next } from 'react-i18next'; 3 | 4 | import de from '../locales/de.json'; 5 | import en from '../locales/en.json'; 6 | import es from '../locales/es.json'; 7 | import fr from '../locales/fr.json'; 8 | import id from '../locales/id.json'; 9 | import it from '../locales/it.json'; 10 | import pt from '../locales/pt.json'; 11 | import pl from '../locales/pl.json'; 12 | import ta from '../locales/ta.json'; 13 | import br from '../locales/br.json'; 14 | import ru from '../locales/ru.json'; 15 | 16 | i18n 17 | // use react-i18next 18 | // doc: https://react.i18next.com/ 19 | .use(initReactI18next) 20 | // init i18next 21 | // for all options read: https://www.i18next.com/overview/configuration-options 22 | .init({ 23 | load: 'languageOnly', 24 | fallbackLng: { 25 | br: ['fr'], 26 | default: ['en'], 27 | }, 28 | interpolation: { 29 | escapeValue: false, // not needed for react as it escapes by default 30 | }, 31 | resources: { 32 | en: { 33 | translation: en, 34 | }, 35 | fr: { 36 | translation: fr, 37 | }, 38 | es: { 39 | translation: es, 40 | }, 41 | id: { 42 | translation: id, 43 | }, 44 | pt: { 45 | translation: pt, 46 | }, 47 | de: { 48 | translation: de, 49 | }, 50 | it: { 51 | translation: it, 52 | }, 53 | pl: { 54 | translation: pl, 55 | }, 56 | ta: { 57 | translation: ta, 58 | }, 59 | br: { 60 | translation: br, 61 | }, 62 | ru: { 63 | translation: ru, 64 | }, 65 | }, 66 | }); 67 | 68 | export default i18n; 69 | -------------------------------------------------------------------------------- /kmfrontend/src/frontend/components/karas/CriteriasList.scss: -------------------------------------------------------------------------------- 1 | @use '../../styles/variables'; 2 | 3 | .criteriasContainer { 4 | height: 100%; 5 | overflow-y: scroll; 6 | > *:last-child { 7 | margin-bottom: 2em; 8 | } 9 | } 10 | 11 | .criterias-line { 12 | margin: 1em; 13 | display: flex; 14 | align-items: center; 15 | 16 | .criterias-type-smart-label { 17 | margin-top: 0.5em; 18 | margin-left: 0.5em; 19 | margin-right: 0.5em; 20 | } 21 | 22 | input { 23 | margin-left: 0.5em; 24 | margin-right: 0.5em; 25 | border: 0; 26 | box-shadow: none; 27 | font-size: 1rem; 28 | background-color: #5a5a5a; 29 | outline: none; 30 | color: #ccc; 31 | } 32 | 33 | input[type='number'] { 34 | width: 5em; 35 | } 36 | 37 | select { 38 | margin-left: 0.5em; 39 | margin-right: 0.5em; 40 | } 41 | } 42 | 43 | .criteriasDescription { 44 | margin: 1em; 45 | font-weight: bold; 46 | } 47 | 48 | .criterias-input { 49 | margin: 1em; 50 | 51 | select { 52 | margin-bottom: 1em; 53 | } 54 | 55 | .criteriasValContainer { 56 | display: flex; 57 | flex-direction: column; 58 | > .input-blc { 59 | font-weight: bold; 60 | padding: 4px !important; 61 | } 62 | > .UI-autocomplete { 63 | flex-grow: 1; 64 | } 65 | > .btn:last-child { 66 | margin-top: 0.5em; 67 | border-top-left-radius: 0; 68 | border-bottom-left-radius: 0; 69 | } 70 | } 71 | } 72 | 73 | .list-group-item.liTag { 74 | min-height: auto; 75 | } 76 | 77 | .list-group-item.liTag > .actionDiv { 78 | padding: 0; 79 | } 80 | .list-group-item.liTag > div { 81 | padding-top: 0; 82 | padding-bottom: 0; 83 | } 84 | -------------------------------------------------------------------------------- /kmfrontend/src/frontend/styles/fonts/Lato/stylesheet.scss: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: 'Lato'; 3 | src: url('/src/frontend/styles/fonts/Lato/Lato-Regular.eot'); 4 | src: 5 | url('/src/frontend/styles/fonts/Lato/Lato-Regular.eot?#iefix') format('embedded-opentype'), 6 | url('/src/frontend/styles/fonts/Lato/Lato-Regular.woff2') format('woff2'), 7 | url('/src/frontend/styles/fonts/Lato/Lato-Regular.woff') format('woff'), 8 | url('/src/frontend/styles/fonts/Lato/Lato-Regular.ttf') format('truetype'); 9 | font-weight: normal; 10 | font-style: normal; 11 | font-display: swap; 12 | } 13 | 14 | @font-face { 15 | font-family: 'Lato'; 16 | src: url('/src/frontend/styles/fonts/Lato/Lato-Bold.eot'); 17 | src: 18 | url('/src/frontend/styles/fonts/Lato/Lato-Bold.eot?#iefix') format('embedded-opentype'), 19 | url('/src/frontend/styles/fonts/Lato/Lato-Bold.woff2') format('woff2'), 20 | url('/src/frontend/styles/fonts/Lato/Lato-Bold.woff') format('woff'), 21 | url('/src/frontend/styles/fonts/Lato/Lato-Bold.ttf') format('truetype'); 22 | font-weight: bold; 23 | font-style: normal; 24 | font-display: swap; 25 | } 26 | 27 | @font-face { 28 | font-family: 'Lato'; 29 | src: url('/src/frontend/styles/fonts/Lato/Lato-Light.eot'); 30 | src: 31 | url('/src/frontend/styles/fonts/Lato/Lato-Light.eot?#iefix') format('embedded-opentype'), 32 | url('/src/frontend/styles/fonts/Lato/Lato-Light.woff2') format('woff2'), 33 | url('/src/frontend/styles/fonts/Lato/Lato-Light.woff') format('woff'), 34 | url('/src/frontend/styles/fonts/Lato/Lato-Light.ttf') format('truetype'); 35 | font-weight: 300; 36 | font-style: normal; 37 | font-display: swap; 38 | } 39 | -------------------------------------------------------------------------------- /kmfrontend/src/frontend/components/generic/Switch.scss: -------------------------------------------------------------------------------- 1 | .switch-ui { 2 | font-family: Lato; 3 | margin-top: 5px; 4 | 5 | input { 6 | display: none; 7 | } 8 | 9 | .switch-ui--control { 10 | display: inline-block; 11 | position: relative; 12 | background: #0000004d; 13 | color: #ffffff99; 14 | line-height: 1.5em; 15 | width: 4em; 16 | white-space: nowrap; 17 | overflow: hidden; 18 | border-radius: 0.75em; 19 | box-shadow: 0 0 5px #0000004d; 20 | border: 1px solid white; 21 | transition: all ease 0.5s; 22 | padding-bottom: 0.1em; 23 | 24 | span { 25 | display: block; 26 | position: absolute; 27 | width: 1em; 28 | height: 1em; 29 | border-radius: 50%; 30 | background: #ffffff99; 31 | top: 0.3em; 32 | left: 0.25em; 33 | transition: left ease 0.5s; 34 | box-shadow: inset 1px 1px 1px 0px #0000004d; 35 | transition: all ease 0.5s; 36 | } 37 | 38 | &:before, 39 | &:after { 40 | display: inline-block; 41 | width: 2.5em; 42 | margin: 0 0 0 0.5em; 43 | transform: translateX(-100%); 44 | transition: transform ease 0.5s; 45 | } 46 | &:before { 47 | content: attr(data-text-on); 48 | text-align: left; 49 | } 50 | &:after { 51 | content: attr(data-text-off); 52 | text-align: right; 53 | } 54 | } 55 | 56 | // Active effect 57 | 58 | input:checked + .switch-ui--control { 59 | color: white; 60 | background: #57bb00; 61 | box-shadow: 0 0 5px #b7ff27cc; 62 | 63 | &:before, 64 | &:after { 65 | transform: translateX(0); 66 | } 67 | span { 68 | left: 2.65em; 69 | top: 0.25em; 70 | background: white; 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /migrations/20250727000000.do.changeSearchableFieldsInAllKaras.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE IF NOT EXISTS all_karas_sortable ( 2 | fk_kid UUID NOT NULL, 3 | titles TEXT, 4 | langs TEXT, 5 | songtypes TEXT, 6 | series_singergroups_singers TEXT 7 | ); 8 | 9 | TRUNCATE all_karas_sortable CASCADE; 10 | 11 | DO $$ 12 | BEGIN 13 | IF EXISTS 14 | ( SELECT 1 15 | FROM information_schema.tables 16 | WHERE table_schema = 'public' 17 | AND table_name = 'all_karas' 18 | ) 19 | THEN 20 | INSERT INTO all_karas_sortable 21 | SELECT 22 | pk_kid, 23 | titles_sortable, 24 | languages_sortable, 25 | songtypes_sortable, 26 | serie_singergroup_singer_sortable 27 | FROM all_karas; 28 | END IF ; 29 | END 30 | $$ ; 31 | 32 | CREATE UNIQUE INDEX IF NOT EXISTS idx_aks_kid 33 | ON all_karas_sortable(fk_kid); 34 | 35 | CREATE INDEX IF NOT EXISTS idx_aks_series_singergroups_singers 36 | ON all_karas_sortable(series_singergroups_singers); 37 | 38 | CREATE INDEX IF NOT EXISTS idx_aks_langs 39 | ON all_karas_sortable(langs); 40 | 41 | CREATE INDEX IF NOT EXISTS idx_aks_titles 42 | ON all_karas_sortable(titles); 43 | 44 | CREATE INDEX IF NOT EXISTS idx_aks_songtypes 45 | ON all_karas_sortable(songtypes); 46 | 47 | ALTER TABLE IF EXISTS all_karas DROP COLUMN IF EXISTS titles_sortable; 48 | ALTER TABLE IF EXISTS all_karas DROP COLUMN IF EXISTS languages_sortable; 49 | ALTER TABLE IF EXISTS all_karas DROP COLUMN IF EXISTS songtypes_sortable; 50 | ALTER TABLE IF EXISTS all_karas DROP COLUMN IF EXISTS serie_singergroup_singer_sortable; -------------------------------------------------------------------------------- /kmfrontend/src/frontend/components/WelcomePageArticle.tsx: -------------------------------------------------------------------------------- 1 | import '../styles/start/WelcomePageArticle.scss'; 2 | 3 | import DOMPurify from 'dompurify'; 4 | import { useCallback, useState } from 'react'; 5 | 6 | import { News } from '../types/news'; 7 | 8 | interface IProps { 9 | article: News; 10 | } 11 | 12 | function WelcomePageArticle(props: IProps) { 13 | const [open, setOpen] = useState(false); 14 | const [containerHeight, setContainerHeight] = useState(0); 15 | const [bodyHeight, setBodyHeight] = useState(0); 16 | 17 | // useRef doen't work, see https://stackoverflow.com/a/67906087 18 | const containerRef = useCallback((node: HTMLDivElement | null) => { 19 | if (node !== null) { 20 | setContainerHeight(node.getBoundingClientRect().height); 21 | } 22 | }, []); 23 | const bodyRef = useCallback((node: HTMLDivElement | null) => { 24 | if (node !== null) { 25 | setBodyHeight(node.getBoundingClientRect().height); 26 | } 27 | }, []); 28 | 29 | const canBeExpanded = () => bodyHeight > containerHeight; 30 | 31 | return ( 32 |
    setOpen(!open)} 37 | > 38 |
    39 | {props.article.title} 40 | {props.article.dateStr} 41 |
    42 |
    43 |
    44 | {!open && canBeExpanded() ?
    : ''} 45 |
    46 |
    47 | ); 48 | } 49 | 50 | export default WelcomePageArticle; 51 | -------------------------------------------------------------------------------- /kmfrontend/src/frontend/components/karas/KaraList.scss: -------------------------------------------------------------------------------- 1 | @use '../../styles/components/blurred-bg'; 2 | 3 | .song-list { 4 | .song { 5 | position: relative; 6 | @include blurred-bg.blurred-bg; 7 | .img-background::before { 8 | border-radius: 16px; 9 | transition: 10 | border-radius, 11 | filter 300ms, 12 | 300ms ease, 13 | ease; 14 | } 15 | margin: 1.5em 1em; 16 | .modal-header { 17 | align-items: flex-start; 18 | position: relative; 19 | text-shadow: 2px 2px 2px #000000bf; 20 | cursor: pointer; 21 | z-index: 2; 22 | .buttons > .btn { 23 | width: 5em; 24 | height: 5em; 25 | i { 26 | font-size: 2.5em; 27 | } 28 | } 29 | } 30 | .transparent-btn { 31 | padding-right: 0.5em; 32 | .fas.fa-chevron-right { 33 | transform: rotate(0); 34 | transition: transform 150ms ease; 35 | } 36 | } 37 | .modal-title { 38 | display: flex; 39 | justify-content: flex-start; 40 | align-items: center; 41 | .tag { 42 | font-size: 1rem; 43 | } 44 | } 45 | transition: margin 300ms ease; 46 | .detailsKara { 47 | margin: 0; 48 | padding: 0; 49 | max-height: 0; 50 | background-color: black; 51 | overflow-y: hidden; 52 | } 53 | &.open { 54 | margin: 1em 0.5em; 55 | &:first-child { 56 | margin-top: 1em; 57 | } 58 | .detailsKara { 59 | max-height: unset; 60 | padding: 1em; 61 | } 62 | > * { 63 | position: relative; 64 | } 65 | .fas.fa-chevron-right { 66 | transform: rotate(90deg); 67 | } 68 | .img-background::before { 69 | border-radius: 0; 70 | filter: blur(0px) contrast(70%) brightness(70%) saturate(75%); 71 | } 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/dao/sql/session.ts: -------------------------------------------------------------------------------- 1 | export const sqlselectSessions = ` 2 | SELECT pk_seid AS seid, 3 | name, 4 | started_at, 5 | ended_at, 6 | private, 7 | COUNT(p.fk_kid)::integer AS played, 8 | COUNT(r.fk_kid)::integer AS requested 9 | FROM session 10 | LEFT JOIN played p ON p.fk_seid = pk_seid 11 | LEFT JOIN requested r on r.fk_seid = pk_seid 12 | GROUP BY pk_seid 13 | ORDER BY started_at DESC 14 | `; 15 | 16 | export const sqlinsertSession = ` 17 | INSERT INTO session(pk_seid, name, started_at, ended_at, private) VALUES( 18 | $1, 19 | $2, 20 | $3, 21 | $4, 22 | $5 23 | ) 24 | `; 25 | 26 | export const sqlreplacePlayed = ` 27 | UPDATE played SET 28 | fk_seid = $2 29 | WHERE fk_seid = $1; 30 | `; 31 | 32 | export const sqlreplaceRequested = ` 33 | UPDATE requested SET 34 | fk_seid = $2 35 | WHERE fk_seid = $1; 36 | `; 37 | 38 | export const sqlupdateSession = ` 39 | UPDATE session SET 40 | name = $2, 41 | started_at = $3, 42 | ended_at = $4, 43 | private = $5 44 | WHERE pk_seid = $1 45 | `; 46 | 47 | export const sqldeleteSession = ` 48 | DELETE FROM session 49 | WHERE pk_seid = $1 50 | `; 51 | 52 | export const sqlcleanSessions = ` 53 | DELETE FROM session 54 | WHERE (SELECT COUNT(fk_kid)::integer FROM played WHERE fk_seid = pk_seid) = 0 55 | AND (SELECT COUNT(fk_kid)::integer FROM requested WHERE fk_seid = pk_seid) = 0 56 | `; 57 | 58 | export const sqlAutoFillSessionEndedAt = ` 59 | UPDATE session 60 | SET ended_at = ( 61 | SELECT p.played_at + (k.duration * '1 second'::interval) AS last_played 62 | FROM played p 63 | LEFT JOIN kara k ON k.pk_kid = p.fk_kid 64 | WHERE p.fk_seid = pk_seid 65 | ORDER BY p.played_at DESC 66 | LIMIT 1 67 | ) 68 | WHERE ended_at IS NULL 69 | AND pk_seid != $1 70 | `; 71 | --------------------------------------------------------------------------------