import { IpcEvents , NuclearMeta } from '@nuclear/core' ; import glob from 'glob' ; import { inject , injectable } from 'inversify' ; import _ from 'lodash' ; import { parseFile , IAudioMetadata } from 'music-metadata' ; import path from 'path' ; import asyncPool from 'tiny-async-pool' ; import { promisify } from 'util' ; import { v4 } from 'uuid' ; import fs from 'fs' ; import { app } from 'electron' ; import LocalLibraryDb from './db' ; import AcousticId from '../acoustic-id' ; import Config from '../config' ; import Logger , { $mainLogger } from '../logger' ; import Window from '../window' ; import LocalTrack from './model/LocalTrack' ; import LocalFolder from './model/LocalFolder' ; export type ProgressHandler = ( progress : number , total : number ) => void ; /**
 * Manage local files, extract metadata directly from files, or get it from acousticId api
 * format all these metadata the nuclear way and store it in a db
 */ @ injectable ( ) class LocalLibrary { private mediaDir : string ; constructor ( @ inject ( Config ) private config , @ inject ( LocalLibraryDb ) private store , @ inject ( AcousticId ) private acousticId , @ inject ( $mainLogger ) private logger , @ inject ( Window ) private window ) { this . mediaDir = path . join ( app . getPath ( 'userData' ) , 'thumbnails' ) ; this . createThumbnailsDirectory ( ) ; } private async createThumbnailsDirectory ( ) { try { await promisify ( fs . stat ) ( this . mediaDir ) ; } catch ( err ) { try { await promisify ( fs . mkdir ) ( this . mediaDir ) ; this . logger . log ( 'Thumbnails cache directory created' ) ; } catch ( err ) { this . logger . error ( 'Error while creating Thumbnails cache directory' ) ; this . logger . error ( err ) ; } } } /**
     * Format metadata from files to nuclear format
     */ private async formatMeta ( { common , format } , { file , folder } ) { return { uuid : v4 ( ) , path : file , folder , duration : format . duration , name : function ( ) { if ( ! _ . isNil ( common . title ) ) { return common . title ; } return path . basename ( file , path . extname ( file ) ) ; } ( ) , position : common . track . no , album : common . album , artist : common . artist || 'unknown' , imageData : common . picture && common . picture [ 0 ] . data , lastScanned : + Date . now ( ) } ; } private async parseMeta ( filesPath , onProgress ? ) { const metas = await Promise . all ( filesPath . map ( ( { file } ) => parseFile ( file ) ) ) ; const formattedMetas = await Promise . all ( filesPath . map ( ( file , i ) => this . formatMeta ( metas [ i ] , file ) ) ) ; if ( this . config . isConnected ) { const formattedMetasWithoutName = formattedMetas . filter ( meta => ! meta . name ) ; if ( formattedMetasWithoutName . length ) { await this . fetchAcousticIdBatch ( formattedMetasWithoutName , onProgress ) ; } } return formattedMetas ; } async getMetas ( filesPath , onProgress ? ) { return this . parseMeta ( filesPath . map ( ( file ) => ( { file } ) ) , onProgress ) ; } /**
     * fetch acousticId metadata 3 by 3 (api limit 3 request / second)
     */ fetchAcousticIdBatch ( metas , onProgress ? ) { let scanProgress = 0 ; const scanTotal = metas . length ; this . logger . log ( `start fetching metadata from acoustic-id api for ${ scanTotal } tracks` ) ; return asyncPool ( 3 , metas , async ( meta ) => { let data ; try { const { results , error } = await this . acousticId . getMetadata ( meta . path as string ) ; if ( results ) { data = results [ 0 ] ; } else if ( error ) { this . logger . error ( `Acoustic ID error (code ${ error . code }): ${ error . message }` ) ; } } catch ( ex ) { this . logger . error ( ex ) ; } if ( data && data . recordings && data . recordings . length ) { meta . name = data . recordings [ 0 ] . name ; meta . artist = data . recordings [ 0 ] . artists ?. [ 0 ] . name || 'unknown' ; } if ( ! meta . name ) { meta . name = path . basename ( meta . path as string ) . split ( '.' ) . shift ( ) as string ; } scanProgress ++ ; if ( onProgress ) { onProgress ( scanProgress , scanTotal ) ; } } ) ; } /**
     * scan folders on local machine, extract metadata and store it to a memory cache
     */ async scanFoldersAndGetMeta ( directories , onProgress ? ) { const filesData = await Promise . all ( _ . flatMap ( this . config . supportedFormats , format => Promise . all ( directories . map ( async ( folder ) => ( { files : await promisify ( glob ) ( `${ folder . path }/**/*.${ format }` ) , folder } ) ) ) ) ) ; const files = filesData . flat ( ) . reduce < Array < { file : string ; folder : LocalFolder ; } > > ( ( acc , item ) => { return item . files . map ( ( filePath ) => ( { file : filePath , folder : item . folder } ) ) . concat ( acc ) ; } , [ ] ) ; const formattedMetas = await this . parseMeta ( files , onProgress ) ; return this . store . updateTracks ( formattedMetas ) ; } async playStartupFile ( filePath ) { try { const metas = await this . getMetas ( [ path . isAbsolute ( filePath ) ? filePath : path . resolve ( process . cwd ( ) , filePath ) ] ) ; this . window . send ( IpcEvents . PLAY_STARTUP_TRACK , metas [ 0 ] ) ; } catch ( err ) { this . logger . error ( 'Error trying to play audio file' ) ; this . logger . error ( err ) ; } } } export default LocalLibrary ;