@@ -19,9 +19,10 @@ type Platform = typeof PLATFORMS[number];
1919type ItemStatus = 'pending' | 'analyzing' | 'processing' | 'done' | 'error' ;
2020type RiskLevel = 'High' | 'Low' ;
2121
22- const DEFAULT_RELEASE_ARTIST = 'Sobelo' ;
23- const DEFAULT_RELEASE_PRODUCER = 'Triple7' ;
24- const DEFAULT_RELEASE_COPYRIGHT = `© ${ new Date ( ) . getUTCFullYear ( ) } Sobelo / Triple7 Music` ;
22+ const DEFAULT_RELEASE_ARTIST = '' ;
23+ const RELEASE_DEFAULTS_STORAGE_KEY = 'spectracleanse_release_defaults' ;
24+ const DEFAULT_RELEASE_PRODUCER = '' ;
25+ const DEFAULT_RELEASE_COPYRIGHT = '' ;
2526
2627// ─────────────────────────────────────────────────────────────────────────────
2728// Types
@@ -63,6 +64,7 @@ interface ReleaseMetadata {
6364 tags : string ;
6465 lyrics : string ;
6566}
67+ type SavedReleaseDefaults = Pick < ReleaseMetadata , 'artist' | 'albumArtist' | 'producer' | 'copyright' | 'genre' | 'description' | 'comment' | 'tags' > ;
6668
6769interface QueueItem {
6870 id : string ;
@@ -98,16 +100,36 @@ function PlanBadge({ plan }: { plan: string }) {
98100
99101const cleanMetadataField = ( value : string | undefined | null ) => String ( value || '' ) . trim ( ) ;
100102
101- const getInitialReleaseMetadata = ( file : File ) : ReleaseMetadata => ( {
103+ const getSavedReleaseDefaults = ( ) : SavedReleaseDefaults | null => {
104+ try {
105+ const raw = localStorage . getItem ( RELEASE_DEFAULTS_STORAGE_KEY ) ;
106+ if ( ! raw ) return null ;
107+ const parsed = JSON . parse ( raw ) as Partial < SavedReleaseDefaults > ;
108+ return {
109+ artist : cleanMetadataField ( parsed . artist ) ,
110+ albumArtist : cleanMetadataField ( parsed . albumArtist ) ,
111+ producer : cleanMetadataField ( parsed . producer ) ,
112+ copyright : cleanMetadataField ( parsed . copyright ) ,
113+ genre : cleanMetadataField ( parsed . genre ) ,
114+ description : cleanMetadataField ( parsed . description ) ,
115+ comment : cleanMetadataField ( parsed . comment ) ,
116+ tags : cleanMetadataField ( parsed . tags ) ,
117+ } ;
118+ } catch {
119+ return null ;
120+ }
121+ } ;
122+
123+ const getInitialReleaseMetadata = ( file : File , savedDefaults : SavedReleaseDefaults | null ) : ReleaseMetadata => ( {
102124 title : file . name . replace ( / \. [ ^ . ] + $ / , '' ) ,
103- artist : DEFAULT_RELEASE_ARTIST ,
104- albumArtist : DEFAULT_RELEASE_ARTIST ,
105- producer : DEFAULT_RELEASE_PRODUCER ,
106- copyright : DEFAULT_RELEASE_COPYRIGHT ,
107- genre : '' ,
108- description : '' ,
109- comment : '' ,
110- tags : '' ,
125+ artist : savedDefaults ?. artist || DEFAULT_RELEASE_ARTIST ,
126+ albumArtist : savedDefaults ?. albumArtist || savedDefaults ?. artist || DEFAULT_RELEASE_ARTIST ,
127+ producer : savedDefaults ?. producer || DEFAULT_RELEASE_PRODUCER ,
128+ copyright : savedDefaults ?. copyright || DEFAULT_RELEASE_COPYRIGHT ,
129+ genre : savedDefaults ?. genre || '' ,
130+ description : savedDefaults ?. description || '' ,
131+ comment : savedDefaults ?. comment || '' ,
132+ tags : savedDefaults ?. tags || '' ,
111133 lyrics : '' ,
112134} ) ;
113135
@@ -727,14 +749,15 @@ export default function App() {
727749
728750 const addFiles = ( files : FileList | File [ ] ) => {
729751 const validExt = / \. ( m p 3 | w a v | f l a c | m 4 a | m p 4 ) $ / i;
752+ const savedDefaults = getSavedReleaseDefaults ( ) ;
730753 const newItems : QueueItem [ ] = Array . from ( files )
731754 . filter ( f => validExt . test ( f . name ) )
732755 . slice ( 0 , 20 - queue . length )
733756 . map ( file => ( {
734757 id : crypto . randomUUID ( ) ,
735758 file,
736759 status : 'pending' as ItemStatus ,
737- seo : getInitialReleaseMetadata ( file ) ,
760+ seo : getInitialReleaseMetadata ( file , savedDefaults ) ,
738761 downloadUrl : null , downloadName : null , report : null , error : null , analysis : null , logs : [ ] ,
739762 } ) ) ;
740763 if ( newItems . length === 0 ) return ;
@@ -756,6 +779,20 @@ export default function App() {
756779 updateItem ( id , { logs : [ ...( queue . find ( i => i . id === id ) ?. logs || [ ] ) , `[${ stamp } ] ${ message } ` ] } ) ;
757780 } ;
758781
782+ const saveReleaseDefaults = ( metadata : ReleaseMetadata ) => {
783+ const defaultsToSave : SavedReleaseDefaults = {
784+ artist : cleanMetadataField ( metadata . artist ) ,
785+ albumArtist : cleanMetadataField ( metadata . albumArtist ) ,
786+ producer : cleanMetadataField ( metadata . producer ) ,
787+ copyright : cleanMetadataField ( metadata . copyright ) ,
788+ genre : cleanMetadataField ( metadata . genre ) ,
789+ description : cleanMetadataField ( metadata . description ) ,
790+ comment : cleanMetadataField ( metadata . comment ) ,
791+ tags : cleanMetadataField ( metadata . tags ) ,
792+ } ;
793+ localStorage . setItem ( RELEASE_DEFAULTS_STORAGE_KEY , JSON . stringify ( defaultsToSave ) ) ;
794+ } ;
795+
759796 const withOperationTimeout = async < T , > ( promise : Promise < T > , timeoutMs : number , timeoutMessage : string ) : Promise < T > => {
760797 return new Promise < T > ( ( resolve , reject ) => {
761798 const timer = window . setTimeout ( ( ) => {
@@ -1238,33 +1275,66 @@ export default function App() {
12381275 < label className = "block text-[10px] font-bold text-slate-500 uppercase mb-1" > Artist</ label >
12391276 < input type = "text" value = { activeItem . seo . artist }
12401277 onChange = { e => updateItem ( activeItem . id , { seo : { ...activeItem . seo , artist : e . target . value } } ) }
1278+ placeholder = "Your artist name"
1279+ className = "w-full bg-slate-950 border border-slate-700 rounded-lg px-3 py-2 text-sm focus:border-cyan-500 outline-none transition-colors" />
1280+ </ div >
1281+ < div >
1282+ < label className = "block text-[10px] font-bold text-slate-500 uppercase mb-1" > Album Artist</ label >
1283+ < input type = "text" value = { activeItem . seo . albumArtist }
1284+ onChange = { e => updateItem ( activeItem . id , { seo : { ...activeItem . seo , albumArtist : e . target . value } } ) }
1285+ placeholder = "Your artist name or group"
12411286 className = "w-full bg-slate-950 border border-slate-700 rounded-lg px-3 py-2 text-sm focus:border-cyan-500 outline-none transition-colors" />
12421287 </ div >
12431288 < div >
12441289 < label className = "block text-[10px] font-bold text-slate-500 uppercase mb-1" > Producer</ label >
12451290 < input type = "text" value = { activeItem . seo . producer }
12461291 onChange = { e => updateItem ( activeItem . id , { seo : { ...activeItem . seo , producer : e . target . value } } ) }
1292+ placeholder = "Producer / label name"
12471293 className = "w-full bg-slate-950 border border-slate-700 rounded-lg px-3 py-2 text-sm focus:border-cyan-500 outline-none transition-colors" />
12481294 </ div >
12491295 < div >
12501296 < label className = "block text-[10px] font-bold text-slate-500 uppercase mb-1" > Copyright</ label >
12511297 < input type = "text" value = { activeItem . seo . copyright }
12521298 onChange = { e => updateItem ( activeItem . id , { seo : { ...activeItem . seo , copyright : e . target . value } } ) }
1299+ placeholder = "© 2026 Your Name or Label"
12531300 className = "w-full bg-slate-950 border border-slate-700 rounded-lg px-3 py-2 text-sm focus:border-cyan-500 outline-none transition-colors" />
12541301 </ div >
12551302 < div >
12561303 < label className = "block text-[10px] font-bold text-slate-500 uppercase mb-1" > Genre</ label >
12571304 < input type = "text" value = { activeItem . seo . genre }
12581305 onChange = { e => updateItem ( activeItem . id , { seo : { ...activeItem . seo , genre : e . target . value } } ) }
1259- placeholder = "trap, metal, cinematic… "
1306+ placeholder = "trap, hip-hop, latin urban "
12601307 className = "w-full bg-slate-950 border border-slate-700 rounded-lg px-3 py-2 text-sm focus:border-cyan-500 outline-none transition-colors" />
12611308 </ div >
12621309 </ div >
1263- < p className = "text-[11px] text-slate-500" > Sobelo / Triple7 values are editable defaults used only when release fields are left blank.</ p >
1310+ < div className = "flex flex-wrap gap-2" >
1311+ < button
1312+ onClick = { ( ) => {
1313+ saveReleaseDefaults ( resolveReleaseMetadata ( activeItem . seo ) ) ;
1314+ addLog ( activeItem . id , 'Release defaults saved.' ) ;
1315+ } }
1316+ className = "px-3 py-1.5 text-xs bg-cyan-700 hover:bg-cyan-600 rounded-lg"
1317+ > Save as My Defaults</ button >
1318+ < button
1319+ onClick = { ( ) => {
1320+ localStorage . removeItem ( RELEASE_DEFAULTS_STORAGE_KEY ) ;
1321+ addLog ( activeItem . id , 'Release defaults cleared.' ) ;
1322+ } }
1323+ className = "px-3 py-1.5 text-xs bg-slate-700 hover:bg-slate-600 rounded-lg"
1324+ > Clear Saved Defaults</ button >
1325+ </ div >
12641326 < div >
12651327 < label className = "block text-[10px] font-bold text-slate-500 uppercase mb-1" > Description</ label >
12661328 < textarea rows = { 3 } value = { activeItem . seo . description }
12671329 onChange = { e => updateItem ( activeItem . id , { seo : { ...activeItem . seo , description : e . target . value } } ) }
1330+ placeholder = "Short release description"
1331+ className = "w-full bg-slate-950 border border-slate-700 rounded-lg px-3 py-2 text-sm focus:border-cyan-500 outline-none resize-none" />
1332+ </ div >
1333+ < div >
1334+ < label className = "block text-[10px] font-bold text-slate-500 uppercase mb-1" > Comment</ label >
1335+ < textarea rows = { 2 } value = { activeItem . seo . comment }
1336+ onChange = { e => updateItem ( activeItem . id , { seo : { ...activeItem . seo , comment : e . target . value } } ) }
1337+ placeholder = "Optional comment / credits"
12681338 className = "w-full bg-slate-950 border border-slate-700 rounded-lg px-3 py-2 text-sm focus:border-cyan-500 outline-none resize-none" />
12691339 </ div >
12701340 < button onClick = { async ( ) => {
@@ -1311,14 +1381,14 @@ export default function App() {
13111381 < label className = "block text-[10px] font-bold text-slate-500 uppercase mb-1" > Tags (comma-separated)</ label >
13121382 < input type = "text" value = { activeItem . seo . tags }
13131383 onChange = { e => updateItem ( activeItem . id , { seo : { ...activeItem . seo , tags : e . target . value } } ) }
1314- placeholder = "trap, heavy metal, original… "
1384+ placeholder = "comma-separated tags "
13151385 className = "w-full bg-slate-950 border border-slate-700 rounded-lg px-3 py-2 text-sm focus:border-cyan-500 outline-none transition-colors" />
13161386 </ div >
13171387 < div >
13181388 < label className = "block text-[10px] font-bold text-slate-500 uppercase mb-1" > Lyrics</ label >
13191389 < textarea rows = { 3 } value = { activeItem . seo . lyrics }
13201390 onChange = { e => updateItem ( activeItem . id , { seo : { ...activeItem . seo , lyrics : e . target . value } } ) }
1321- placeholder = "Optional lyrics to write when available… "
1391+ placeholder = "Optional lyrics"
13221392 className = "w-full bg-slate-950 border border-slate-700 rounded-lg px-3 py-2 text-sm focus:border-cyan-500 outline-none resize-none" />
13231393 </ div >
13241394 </ div >
@@ -1371,7 +1441,7 @@ export default function App() {
13711441 comment : activeItem . seo . comment ,
13721442 lyrics : activeItem . seo . lyrics || activeItem . analysis ?. lyrics || '' ,
13731443 tags : activeItem . seo . tags ,
1374- publisher : 'Triple7 Music ',
1444+ publisher : activeItem . seo . producer || ' ',
13751445 } ) ;
13761446 rewrittenBlob = blob ;
13771447 addLog ( itemId , `ID3 frames written: ${ frameReport . writtenFrames . join ( ', ' ) || 'none' } ${ frameReport . skippedFrames . length ? ` | skipped: ${ frameReport . skippedFrames . join ( ', ' ) } ` : '' } ` ) ;
0 commit comments