Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions src/features/audio/audioFormat.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
export type AudioFormat = "mp3";

export interface AudioFormatInfo {
id: AudioFormat;
extension: ".mp3";
mimeType: "audio/mpeg";
}

const audioFormatInfo = {
mp3: {
id: "mp3",
extension: ".mp3",
mimeType: "audio/mpeg",
},
} as const satisfies Record<AudioFormat, AudioFormatInfo>;

export const getAudioFormatInfo = (format: AudioFormat): AudioFormatInfo => audioFormatInfo[format];

export const getAudioUploadAccept = () =>
Object.values(audioFormatInfo)
.flatMap(({ extension, mimeType }) => [extension, mimeType])
.join(",");

export const hasAudioExtension = (filename: string, format: AudioFormat) =>
filename.toLowerCase().endsWith(getAudioFormatInfo(format).extension);

export const withoutAudioExtension = (filename: string, format: AudioFormat) => {
const { extension } = getAudioFormatInfo(format);
return hasAudioExtension(filename, format) ? filename.slice(0, -extension.length) : filename;
};

export const withAudioExtension = (base: string, format: AudioFormat) =>
hasAudioExtension(base, format) ? base : `${base}${getAudioFormatInfo(format).extension}`;

export const normalizeAudioFilename = (filename: string, format: AudioFormat) => {
if (hasAudioExtension(filename, format)) return filename;
const basename = filename.replace(/\.[^.]+$/, "") || "track";
return withAudioExtension(basename, format);
};
16 changes: 12 additions & 4 deletions src/features/audio/audioMetadataIO.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,13 @@ import {
import { audioMetadataSchema } from "@/features/audio/metadata";
import { parseTrackTagNumber, toGenreString, type UploadedTrack } from "@/features/audio/mp3Utils";
import type { AudioMetadata, TagiumFile } from "@/features/library/types";
import {
getAudioFormatInfo,
withAudioExtension,
withoutAudioExtension,
} from "@/features/audio/audioFormat";
import {
getMp3AdmissionError,
MP3_MIME_TYPE,
normalizeMp3File,
normalizeMp3Filename,
} from "@/features/audio/mp3Compatibility";
Expand Down Expand Up @@ -171,7 +175,7 @@ const parseUploadedTrack = (file: File) =>
})) ?? [];

const metadata = yield* decodeReadMetadata({
filename: normalizeMp3Filename(file.name).replace(/\.mp3$/i, ""),
filename: withoutAudioExtension(normalizeMp3Filename(file.name), "mp3"),
title: mp3tag.tags.title || "",
artist: mp3tag.tags.artist || "",
album: mp3tag.tags.album || "",
Expand All @@ -187,6 +191,7 @@ const parseUploadedTrack = (file: File) =>
return {
file: {
id,
format: "mp3",
file: normalizedFile,
originalFile: file,
filename: normalizedFile.name,
Expand All @@ -209,6 +214,7 @@ const parseUploadedTrack = (file: File) =>
return Effect.succeed({
file: {
id,
format: "mp3",
file,
originalFile: file,
filename: file.name,
Expand Down Expand Up @@ -297,9 +303,11 @@ const writeMetadata = (fileToUpdate: TagiumFile, newTags: AudioMetadata) =>

return new File(
[new Uint8Array(buffer)],
metadataToWrite.filename ? `${metadataToWrite.filename}.mp3` : fileToUpdate.filename,
metadataToWrite.filename
? withAudioExtension(metadataToWrite.filename, fileToUpdate.format)
: fileToUpdate.filename,
{
type: MP3_MIME_TYPE,
type: getAudioFormatInfo(fileToUpdate.format).mimeType,
},
);
});
Expand Down
17 changes: 10 additions & 7 deletions src/features/audio/mp3Compatibility.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
export const MP3_MIME_TYPE = "audio/mpeg";
import {
getAudioFormatInfo,
hasAudioExtension,
normalizeAudioFilename,
} from "@/features/audio/audioFormat";

export const MP3_MIME_TYPE = getAudioFormatInfo("mp3").mimeType;

const bitrateKbps = {
mpeg1Layer1: [0, 32, 64, 96, 128, 160, 192, 224, 256, 288, 320, 352, 384, 416, 448],
Expand Down Expand Up @@ -120,7 +126,7 @@ const startsWithAscii = (bytes: Uint8Array, value: string, offset = 0) =>

export const getMp3AdmissionError = (file: File, bytes: Uint8Array) => {
if (bytes.length === 0) return `${file.name} is empty. Choose a valid mp3 file.`;
if (!/\.mp3$/i.test(file.name)) {
if (!hasAudioExtension(file.name, "mp3")) {
return `${file.name} is not an mp3. tagium currently supports mp3 files only.`;
}
const knownUnsupported =
Expand All @@ -135,13 +141,10 @@ export const getMp3AdmissionError = (file: File, bytes: Uint8Array) => {
return `${file.name} is not a valid mp3. The file may be corrupt or renamed.`;
};

export const normalizeMp3Filename = (filename: string) => {
const basename = filename.replace(/\.[^.]+$/, "") || "track";
return `${basename}.mp3`;
};
export const normalizeMp3Filename = (filename: string) => normalizeAudioFilename(filename, "mp3");

export const normalizeMp3File = (file: File) =>
file.type === MP3_MIME_TYPE && /\.mp3$/i.test(file.name)
file.type === MP3_MIME_TYPE && hasAudioExtension(file.name, "mp3")
? file
: new File([file], normalizeMp3Filename(file.name), {
type: MP3_MIME_TYPE,
Expand Down
8 changes: 6 additions & 2 deletions src/features/editor/TrackMetadataEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { Input } from "@/components/ui/input";
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
import CoverArt from "@/features/editor/coverArt";
import { isValidFilenameBase, sanitizeFilenameBase } from "@/features/library/filename";
import { getAudioFormatInfo } from "@/features/audio/audioFormat";
import { getSampleTrack, type SampleTrackMetadata } from "@/features/editor/sampleMetadata";
import { getTrackFailureDisplay } from "@/features/workspace/systemFailure";
import type { AlbumGroup, AudioMetadata, TagiumFile } from "@/features/library/types";
Expand Down Expand Up @@ -90,6 +91,7 @@ function TrackFilenameHeader({
filenamePlaceholder,
filenameInvalid,
filenameRegistration,
extension,
failure,
}: {
syncFilenames: boolean;
Expand All @@ -98,6 +100,7 @@ function TrackFilenameHeader({
filenamePlaceholder: string;
filenameInvalid: boolean;
filenameRegistration: UseFormRegisterReturn<"filename">;
extension: string;
failure: TrackFailure | null;
}) {
return (
Expand All @@ -113,7 +116,7 @@ function TrackFilenameHeader({
</TooltipTrigger>
<TooltipContent>filename follows the title</TooltipContent>
</Tooltip>
<span className="shrink-0 select-none text-muted-foreground/70">.mp3</span>
<span className="shrink-0 select-none text-muted-foreground/70">{extension}</span>
</h2>
) : (
<label className="inline-flex min-w-0 max-w-full items-center text-base font-semibold max-lg:[@media(max-height:700px)]:text-sm lg:text-lg">
Expand All @@ -131,7 +134,7 @@ function TrackFilenameHeader({
placeholder={filenamePlaceholder}
/>
</span>
<span className="shrink-0 select-none text-muted-foreground/70">.mp3</span>
<span className="shrink-0 select-none text-muted-foreground/70">{extension}</span>
</label>
)}
</div>
Expand Down Expand Up @@ -414,6 +417,7 @@ function LoadedTrackMetadataEditor({
filenamePlaceholder={placeholder.filename}
filenameInvalid={filenameInvalid}
filenameRegistration={filenameRegistration}
extension={getAudioFormatInfo(selectedFile.format).extension}
failure={failure}
/>
<div className="flex-1 min-h-0 overflow-y-auto p-3 pb-3 max-lg:[@media(max-height:700px)]:p-2 lg:p-6 lg:pb-28">
Expand Down
13 changes: 10 additions & 3 deletions src/features/editor/useTrackEditorSession.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import type {
MetadataPatch,
TagiumFile,
} from "@/features/library/types";
import { withAudioExtension } from "@/features/audio/audioFormat";

type PreviewField = "filename" | "title" | "artist";

Expand Down Expand Up @@ -64,7 +65,9 @@ const applyMetadataPatch = (metadata: AudioMetadata, patch: MetadataPatch): Audi
});

const getFilenameFromPatch = (file: TagiumFile, patch: MetadataPatch) =>
hasOwn(patch, "filename") && patch.filename ? `${patch.filename}.mp3` : file.filename;
hasOwn(patch, "filename") && patch.filename
? withAudioExtension(patch.filename, file.format)
: file.filename;

const withPendingMetadataPatch = (
file: TagiumFile,
Expand Down Expand Up @@ -261,7 +264,9 @@ export const useTrackEditorSession = ({
? withPendingMetadataPatch(
{
...file,
filename: newTags.filename ? `${newTags.filename}.mp3` : file.filename,
filename: newTags.filename
? withAudioExtension(newTags.filename, file.format)
: file.filename,
metadata,
status: "pending" as const,
},
Expand Down Expand Up @@ -306,7 +311,9 @@ export const useTrackEditorSession = ({
bitrate: file.metadata?.bitrate || 0,
sampleRate: file.metadata?.sampleRate || 0,
},
filename: newTags.filename ? `${newTags.filename}.mp3` : file.filename,
filename: newTags.filename
? withAudioExtension(newTags.filename, file.format)
: file.filename,
downloadError: message,
},
createSubmittedMetadataPatch(newTags),
Expand Down
7 changes: 5 additions & 2 deletions src/features/export/downloadLibrary.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import filenamify from "filenamify";
import type { AlbumGroup, TagiumFile } from "@/features/library/types";
import { isValidFilenameBase } from "@/features/library/filename";
import { normalizeMp3Filename } from "@/features/audio/mp3Compatibility";
import { normalizeAudioFilename, withAudioExtension } from "@/features/audio/audioFormat";

export interface DownloadZipEntry {
path: string;
Expand Down Expand Up @@ -120,7 +120,10 @@ const addTrackEntry = (
track: TagiumFile | undefined,
) => {
if (!track || !isTrackReadyForDownload(track) || !track.file) return;
const filename = normalizeMp3Filename(cleanPathPart(track.filename, "track.mp3"));
const filename = normalizeAudioFilename(
cleanPathPart(track.filename, withAudioExtension("track", track.format)),
track.format,
);
entries.push({
path: uniquePath(`${folderPath}/${filename}`, usedPaths),
file: track.file,
Expand Down
3 changes: 2 additions & 1 deletion src/features/import/LandingScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import { useRef, useState, type ReactNode } from "react";
import { Music4, Upload } from "lucide-react";
import { cn } from "@/lib/utils";
import { getAudioUploadAccept } from "@/features/audio/audioFormat";

interface LandingScreenProps {
active: boolean;
Expand Down Expand Up @@ -62,7 +63,7 @@ export default function LandingScreen({ active, children, onAudioUpload }: Landi
<input
ref={fileInputRef}
type="file"
accept=".mp3,audio/mpeg"
accept={getAudioUploadAccept()}
multiple
className="hidden"
onChange={handleFileChange}
Expand Down
3 changes: 2 additions & 1 deletion src/features/import/audioUpload.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { useId, useRef } from "react";
import { Upload } from "lucide-react";
import { getAudioUploadAccept } from "@/features/audio/audioFormat";

interface AudioUploadProps {
onAudioUpload: (audio: File[]) => void;
Expand Down Expand Up @@ -34,7 +35,7 @@ export default function AudioUpload({ onAudioUpload }: AudioUploadProps) {
type="file"
id={inputId}
className="hidden"
accept=".mp3,audio/mpeg"
accept={getAudioUploadAccept()}
onChange={handleAudioUpload}
multiple
ref={fileInputRef}
Expand Down
13 changes: 8 additions & 5 deletions src/features/import/downloadTrack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import type {
MetadataPatch,
TagiumFile,
} from "@/features/library/types";
import { withAudioExtension, withoutAudioExtension } from "@/features/audio/audioFormat";

export type DownloadRequest = NonNullable<TagiumFile["downloadRequest"]>;

Expand Down Expand Up @@ -107,8 +108,8 @@ export type SoundCloudSetDownloadWorkflowDeps = PlaylistDownloadWorkflowDeps;

const filenameFromTitle = (title: string) => {
const filename = filenamify(title.trim(), { replacement: "-" });
if (filename) return `${filename}.mp3`;
return "downloading-track.mp3";
if (filename) return withAudioExtension(filename, "mp3");
return withAudioExtension("downloading-track", "mp3");
};

export const titleFromSourceUrl = (sourceUrl: string) => {
Expand Down Expand Up @@ -139,7 +140,7 @@ export const createDownloadMetadata = ({
duration?: number;
trackNumber?: number;
}): AudioMetadata => ({
filename: filenameFromTitle(title).replace(/\.mp3$/i, ""),
filename: withoutAudioExtension(filenameFromTitle(title), "mp3"),
title,
artist,
album,
Expand All @@ -160,7 +161,8 @@ export const createPendingDownloadTrack = (
pendingMetadataPatch?: MetadataPatch,
): PendingDownloadTrack => ({
id,
filename: `${metadata.filename}.mp3`,
format: "mp3",
filename: withAudioExtension(metadata.filename, "mp3"),
status: "pending",
downloadStatus: "downloading",
downloadRequest,
Expand Down Expand Up @@ -226,7 +228,8 @@ export const fetchImportedCover = async (

export const createQueuedDownloadTrack = (file: PendingDownloadTrack): QueuedDownloadTrack => ({
fileId: file.id,
title: file.metadata.title || file.filename.replace(/\.mp3$/i, "") || "downloading audio",
title:
file.metadata.title || withoutAudioExtension(file.filename, file.format) || "downloading audio",
downloadRequest: file.downloadRequest,
});

Expand Down
13 changes: 9 additions & 4 deletions src/features/library/fileMetadataOps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import type {
MetadataPatch,
TagiumFile,
} from "@/features/library/types";
import { withAudioExtension } from "@/features/audio/audioFormat";

export interface DownloadedTrackHydration {
hydratedFile: TagiumFile;
Expand Down Expand Up @@ -240,14 +241,15 @@ export function applySyncedFilenamesToFiles(files: TagiumFile[], trackIds?: stri

const syncedFilename = filenamify(file.metadata.title, { replacement: "-" });
if (!syncedFilename) return file;
if (file.filename === `${syncedFilename}.mp3` && file.metadata.filename === syncedFilename) {
const filename = withAudioExtension(syncedFilename, file.format);
if (file.filename === filename && file.metadata.filename === syncedFilename) {
return file;
}

return markPendingMetadataPatch(
{
...file,
filename: `${syncedFilename}.mp3`,
filename,
status: file.status === "saved" ? "pending" : file.status,
metadata: {
...file.metadata,
Expand Down Expand Up @@ -436,7 +438,10 @@ export function prepareDownloadedTrackHydration(
...currentFile,
file: parsedFile.file,
originalFile: parsedFile.originalFile,
filename: nextMetadata?.filename ? `${nextMetadata.filename}.mp3` : parsedFile.filename,
format: parsedFile.format,
filename: nextMetadata?.filename
? withAudioExtension(nextMetadata.filename, parsedFile.format)
: parsedFile.filename,
metadata: nextMetadata,
downloadStatus: "ready",
downloadError: parsedFile.downloadError,
Expand Down Expand Up @@ -466,7 +471,7 @@ export function resolveDownloadedTrackHydrationWrite(
const nextFile = latestFormMetadata
? {
...latestFile,
filename: `${latestFormMetadata.filename}.mp3`,
filename: withAudioExtension(latestFormMetadata.filename, latestFile.format),
metadata: latestFormMetadata,
}
: latestFile;
Expand Down
3 changes: 2 additions & 1 deletion src/features/library/metadataCleanup.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { sanitizeFilenameBase } from "@/features/library/filename";
import type { AlbumGroup, MetadataPatch, TagiumFile } from "@/features/library/types";
import { withAudioExtension } from "@/features/audio/audioFormat";

const removableLabels = [
"official audio",
Expand Down Expand Up @@ -160,7 +161,7 @@ export function findMetadataCleanupSuggestions(
beforeTitle: file.metadata.title,
afterTitle: cleanup.afterTitle,
beforeFilename: file.filename,
afterFilename: `${sanitizeFilenameBase(cleanup.afterTitle)}.mp3`,
afterFilename: withAudioExtension(sanitizeFilenameBase(cleanup.afterTitle), file.format),
reasons: cleanup.reasons,
},
];
Expand Down
Loading
Loading