Skip to content

Commit

Permalink
import music by tags
Browse files Browse the repository at this point in the history
  • Loading branch information
mebtte committed Mar 7, 2024
1 parent a8d70d4 commit 3408f51
Show file tree
Hide file tree
Showing 12 changed files with 182 additions and 158 deletions.
6 changes: 2 additions & 4 deletions apps/cli/src/commands/fix_data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,13 +45,11 @@ export default async ({ data }: { data: string }) => {
let spinner: Spinner;

// eslint-disable-next-line prefer-const
spinner = createSpinner();
spinner.start({ text: "Fixing music's year..." });
spinner = createSpinner().start({ text: "Fixing music's year..." });
await fixMusicYear();
spinner.success({ text: "Music's year has fixed" });

spinner = createSpinner();
spinner.start({ text: 'Fixing DB snapshots...' });
spinner = createSpinner().start({ text: 'Fixing DB snapshots...' });
await fixDBSnapshots();
spinner.success({ text: 'DB snapshots have fixed' });

Expand Down
107 changes: 61 additions & 46 deletions apps/cli/src/commands/import_music.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,24 +24,13 @@ import {
MusicSingerRelationProperty,
} from '@/constants/db_definition';
import { getAssetDirectory, updateConfig } from '../config';

/**
* 文件格式, singer1,singer2 - name.format
* @author mebtte<hi@mebtte.com>
*/
const MATCH_FILENAME = /^((([^,]+)(,?))+)(\s+)-(\s+)(.+)\.(\S+)$/;
import getMusicFileMetadata, {
Metadata,
} from '#/utils/get_music_file_metadata';

let successful = 0;
let ignored = 0;

/**
* 合并多个空格以及移除头尾空格
* @author mebtte<hi@mebtte.com>
*/
function handleSpace(s: string) {
return s.replace(/\s+/g, ' ').trim();
}

/**
* 检查音乐是否已存在
* @author mebtte<hi@mebtte.com>
Expand Down Expand Up @@ -88,7 +77,7 @@ async function checkMusicExist({
}

async function importFile(
file: string,
filepath: string,
{
skipExistenceCheck,
uid,
Expand All @@ -97,46 +86,38 @@ async function importFile(
uid: string;
},
) {
const spinner = createSpinner(file);
spinner.start();

/**
* 检查文件名
* @author mebtte<hi@mebtte.com>
*/
if (!MATCH_FILENAME.test(path.parse(file).base)) {
ignored += 1;
return spinner.warn({
text: `[ ${file} ] isn't a valid filename, ignored`,
});
}
const spinner = createSpinner(filepath).start();

/**
* 检查文件类型
* @author mebtte<hi@mebtte.com>
*/
const ft = await fileType.fromFile(file);
const ft = await fileType.fromFile(filepath);
const { acceptTypes } = ASSET_TYPE_MAP[AssetType.MUSIC];
if (ft && acceptTypes.includes(ft.mime)) {
const [singerString, originalName] = path.parse(file).name.split(' - ');
const name = handleSpace(originalName);
const singers = singerString
.split(',')
.map((s) => handleSpace(s))
.filter((s) => s.length > 0);

let musicTag: Metadata;
try {
musicTag = await getMusicFileMetadata(filepath);
} catch (error) {
ignored += 1;
return spinner.warn({
text: `[ ${filepath} ] can not be parsed and been ignored`,
});
}
const name = musicTag.title || 'Unknown';
const singers = musicTag.artist?.split(',') || ['Unknown'];
if (!skipExistenceCheck) {
const exist = await checkMusicExist({ singers, name });
if (exist) {
ignored += 1;
return spinner.warn({
text: `[ ${file} ] has been database already and ignored, using [ --skip-existence-check ] will skip existence check`,
text: `[ ${filepath} ] has been in database already and ignored, using [ --skip-existence-check ] will skip existence check`,
});
}
}

const fileData = await fs.readFile(file);
const assetName = md5(fileData) + path.parse(file).ext;
const fileData = await fs.readFile(filepath);
const assetName = md5(fileData) + path.parse(filepath).ext;
await fs.writeFile(
`${getAssetDirectory(AssetType.MUSIC)}/${assetName}`,
fileData,
Expand Down Expand Up @@ -169,14 +150,46 @@ async function importFile(
);
}

spinner.success({
text: `[ ${file} ] imported`,
});
if (musicTag.year) {
await getDB().run(
`
UPDATE ${MUSIC_TABLE_NAME} SET ${MusicProperty.YEAR} = ?
WHERE ${MusicProperty.ID} = ?
`,
[musicTag.year, id],
);
}

if (musicTag.picture) {
const buffer = Buffer.from(
musicTag.picture.dataURI.split(',')[1],
'base64',
);
const coverFilename = `${md5(buffer)}.${
musicTag.picture.format.split('/')[1]
}`;
await Promise.all([
fs.writeFile(
`${getAssetDirectory(AssetType.MUSIC_COVER)}/${coverFilename}`,
buffer,
),
getDB().run(
`
UPDATE ${MUSIC_TABLE_NAME} SET ${MusicProperty.COVER} = ?
WHERE ${MusicProperty.ID} = ?
`,
[coverFilename, id],
),
]);
}

successful += 1;
spinner.success({
text: `[ ${filepath} ] imported`,
});
} else {
spinner.warn({ text: `[ ${file} ] isn't a valid format, ignored` });
ignored += 1;
spinner.warn({ text: `[ ${filepath} ] isn't a valid format, ignored` });
}
}

Expand Down Expand Up @@ -217,7 +230,7 @@ async function importDirectory(
}
}

export default async ({
async function importMusic({
source,
data,
uid,
Expand All @@ -229,7 +242,7 @@ export default async ({
uid: string;
recursive: boolean;
skipExistenceCheck: boolean;
}) => {
}) {
updateConfig({ data });

/**
Expand All @@ -252,4 +265,6 @@ export default async ({
text: `Successful ${successful}, ignored ${ignored}`,
});
return process.exit();
};
}

export default importMusic;
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ export default async (ctx: Context) => {
if (
typeof id !== 'string' ||
!id.length ||
// @ts-expect-error
// @ts-expect-error: key is unknown
!Object.values(AllowUpdateKey).includes(key)
) {
return ctx.except(ExceptionCode.WRONG_PARAMETER);
Expand Down
29 changes: 12 additions & 17 deletions apps/cli/src/commands/upgrade_data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { getDB } from '@/db';
import generateRandomString from '#/utils/generate_random_string';

function dropLoginCodeSalt() {
return fs.unlinkSync(`${getConfig().data}/login_code_salt`);
return fsPromises.unlink(`${getConfig().data}/login_code_salt`);
}

function dropLoginCode() {
Expand Down Expand Up @@ -105,15 +105,15 @@ async function addUserPassword() {
}

async function addUserTwoFASecret() {
return getDB().run(
return await getDB().run(
`
ALTER TABLE user ADD twoFASecret TEXT DEFAULT NULL
`,
);
}

async function addUserTokenIdentifier() {
return getDB().run(
return await getDB().run(
`
ALTER TABLE user ADD tokenIdentifier TEXT NOT NULL DEFAULT ''
`,
Expand Down Expand Up @@ -154,38 +154,33 @@ export default async ({ data }: { data: string }) => {

let spinner: Spinner;

spinner = createSpinner();
spinner.start({ text: 'Dropping login code...' });
spinner = createSpinner().start({ text: 'Dropping login code...' });
await dropLoginCode();
spinner.success({ text: 'Login code has dropped' });

spinner = createSpinner();
spinner.start({ text: 'Dropping login code salt...' });
spinner = createSpinner().start({ text: 'Dropping login code salt...' });
await dropLoginCodeSalt();
spinner.success({ text: 'Login code salt has dropped' });

spinner = createSpinner();
spinner.start({ text: 'Renaming user.email to user.username...' });
spinner = createSpinner().start({
text: 'Renaming user.email to user.username...',
});
await renameUserEmailToUsername();
spinner.success({ text: 'user.email has renamed to user.username' });

spinner = createSpinner();
spinner.start({ text: 'Adding user.password...' });
spinner = createSpinner().start({ text: 'Adding user.password...' });
const userList = await addUserPassword();
spinner.success({ text: 'user.password has added' });

spinner = createSpinner();
spinner.start({ text: 'Adding user.twoFASecret...' });
spinner = createSpinner().start({ text: 'Adding user.twoFASecret...' });
await addUserTwoFASecret();
spinner.success({ text: 'user.twoFASecret has added' });

spinner = createSpinner();
spinner.start({ text: 'Adding user.tokenIdentifier...' });
spinner = createSpinner().start({ text: 'Adding user.tokenIdentifier...' });
await addUserTokenIdentifier();
spinner.success({ text: 'user.tokenIdentifier has added' });

spinner = createSpinner();
spinner.start({ text: 'Writting new version of data...' });
spinner = createSpinner().start({ text: 'Writting new version of data...' });
await writeNewVersion();
spinner.success({ text: 'New version of data has wrote' });

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,7 @@ import uploadAsset from '@/server/form/upload_asset';
import createMusic from '@/server/api/create_music';
import { SEARCH_KEYWORD_MAX_LENGTH } from '#/constants/singer';
import updateMusic from '@/server/api/update_music';
import getMusicFileMetadata, {
base64ToCover,
} from '@/utils/get_music_file_metadata';
import getMusicFileMetadata from '#/utils/get_music_file_metadata';
import logger from '@/utils/logger';
import { MUSIC_TYPE_MAP } from '@/constants/music';
import capitalize from '#/utils/capitalize';
Expand All @@ -42,6 +40,7 @@ import playerEventemitter, {
} from '../../../eventemitter';
import { Singer } from './constants';
import upperCaseFirstLetter from '#/utils/upper_case_first_letter';
import { base64ToCover } from './utils';

const maskProps: { style: CSSProperties } = {
style: { zIndex: ZIndex.DIALOG },
Expand Down Expand Up @@ -146,9 +145,11 @@ function CreateMusicDialog() {
});

try {
const { lyric, pictureBase64, year } = await getMusicFileMetadata(
asset,
);
const {
// lyric,
picture,
year,
} = await getMusicFileMetadata(asset);
const updateCover = async (pb: string) => {
const coverBlob = await base64ToCover(pb);
const { id: assetId } = await uploadAsset(
Expand All @@ -164,15 +165,15 @@ function CreateMusicDialog() {
};

await Promise.all([
musicType === MusicType.SONG && lyric
? await updateMusic({
id,
key: AllowUpdateKey.LYRIC,
value: [lyric],
requestMinimalDuration: 0,
})
: null,
pictureBase64 ? updateCover(pictureBase64) : null,
// musicType === MusicType.SONG && lyric
// ? await updateMusic({
// id,
// key: AllowUpdateKey.LYRIC,
// value: [lyric],
// requestMinimalDuration: 0,
// })
// : null,
picture ? updateCover(picture.dataURI) : null,
year
? updateMusic({
id,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { IMAGE_MAX_SIZE } from '#/constants';
import loadImage from '@/utils/load_image';

export async function base64ToCover(base64: string) {
const imgNode = await loadImage(base64);

const size = Math.min(
IMAGE_MAX_SIZE,
imgNode.naturalWidth,
imgNode.naturalHeight,
);
const canvas = document.createElement('canvas');
canvas.width = size;
canvas.height = size;
const context = canvas.getContext('2d')!;

context.drawImage(
imgNode,
(size - imgNode.naturalWidth) / 2,
(size - imgNode.naturalHeight) / 2,
imgNode.naturalWidth,
imgNode.naturalHeight,
);
return await new Promise<Blob>((resolve) =>
canvas.toBlob((blob) => resolve(blob!), 'image/jpeg', 0.8),
);
}
2 changes: 1 addition & 1 deletion apps/pwa/src/utils/custom_audio.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
class CustomAudio<Extra> {
readonly extra: Extra;

private audio: HTMLAudioElement;
private readonly audio: HTMLAudioElement;

constructor({ src, extra }: { src: string; extra: Extra }) {
const audio = new window.Audio();
Expand Down
Loading

0 comments on commit 3408f51

Please sign in to comment.