"use strict";
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
    return new (P || (P = Promise))(function (resolve, reject) {
        function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
        function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
        function step(result) { result.done ? resolve(result.value) : new P(function (resolve) { resolve(result.value); }).then(fulfilled, rejected); }
        step((generator = generator.apply(thisArg, _arguments || [])).next());
    });
};
Object.defineProperty(exports, "__esModule", { value: true });
const path = require("path");
const fs_1 = require("fs");
const mkdirp = require("mkdirp");
const events = require("events");
const storyboard_1 = require("storyboard");
const config_1 = require("../../config");
const Models = require("compactd-models");
const Ffmpeg = require("fluent-ffmpeg");
const FSTree = require("fs-tree-diff");
const walkSync = require('walk-sync');
class Scanner extends events.EventEmitter {
    constructor(library) {
        super();
        this.library = library;
    }
    updateLibrary(pouchDB, props) {
        return __awaiter(this, void 0, void 0, function* () {
            const libraries = new pouchDB('libraries');
            const lib = yield libraries.get(this.library);
            yield libraries.put(Object.assign({}, lib, props));
            storyboard_1.mainStory.debug('scanner', 'Updated library record', {
                attach: { lib, props },
                attachLevel: 'debug'
            });
        });
    }
    scan(pouchDB) {
        return __awaiter(this, void 0, void 0, function* () {
            const libraries = new pouchDB('libraries');
            const lib = yield libraries.get(this.library);
            storyboard_1.mainStory.debug('scanner', 'Starting scan', {
                attach: { lib }
            });
            yield this.updateLibrary(pouchDB, { status: 'scanning' });
            this.path = lib.path;
            const timestamp = Date.now();
            this.walkDirectory();
            const patch = yield this.getPatch();
            yield patch.reduce((acc, entry) => {
                return acc.then(() => this.databaseEntryCreator(pouchDB, entry));
            }, Promise.resolve());
            yield this.serializeEntries(config_1.default.get('dataDirectory'));
            yield this.updateLibrary(pouchDB, { status: 'ready' });
            this.emit('scan_ended');
        });
    }
    walkDirectory() {
        this.entries = walkSync.entries(this.path);
        storyboard_1.mainStory.debug('scanner', `Loaded ${this.entries.length} files to be potentially processed`);
    }
    getSerializedEntries(dir) {
        const file = path.join(dir, `fstree/${path.basename(this.library)}.entries.json`);
        return new Promise((resolve, reject) => {
            if (!fs_1.existsSync(file))
                return resolve([]);
            fs_1.readFile(file, 'utf8', function (err, content) {
                if (err)
                    return reject(err);
                const entries = JSON.parse(content).entries;
                storyboard_1.mainStory.debug('scanner', `Not processing ${entries.length} entries`);
                resolve(entries.map((el) => {
                    return Object.assign({}, el, { isDirectory: () => el.dir });
                }));
            });
        });
    }
    get treePath() {
        const dir = config_1.default.get('dataDirectory');
        return path.join(dir, `fstree/${path.basename(this.library)}.entries.json`);
    }
    serializeEntries(dir) {
        mkdirp.sync(path.join(dir, 'fstree/'));
        const file = path.join(dir, `fstree/${path.basename(this.library)}.entries.json`);
        return new Promise((resolve, reject) => {
            fs_1.writeFile(file, JSON.stringify({ entries: this.entries.map((el) => {
                    return Object.assign({}, el, { dir: el.isDirectory() });
                }) }), function (err) {
                if (err)
                    return reject(err);
                resolve();
            });
        });
    }
    getPatch() {
        return __awaiter(this, void 0, void 0, function* () {
            const serialized = yield this.getSerializedEntries(config_1.default.get('dataDirectory'));
            storyboard_1.mainStory.info('scanner', `Already scanned ${serialized.length} items...`);
            const staled = FSTree.fromEntries(serialized || []);
            const current = FSTree.fromEntries(this.entries || []);
            return staled.calculatePatch(current);
        });
    }
    ffprobe(file) {
        const exts = ['.mp4', '.mp3', '.m4a', '.flac', '.ogg', '.wav', '.wmv', '.alac'];
        return new Promise((resolve, reject) => {
            if (!exts.includes(path.extname(file))) {
                storyboard_1.mainStory.debug('scanner', `Skipping ${file} (unsupported format)`);
                return resolve();
            }
            storyboard_1.mainStory.debug('scanner', `Scanning file ${file}`);
            const ffmpeg = Ffmpeg();
            ffmpeg.input(file).ffprobe((err, data) => {
                if (err)
                    return reject(err);
                resolve(data);
            });
        });
    }
    findOrCreateArtist(pouchDB, name) {
        return __awaiter(this, void 0, void 0, function* () {
            const artist = new pouchDB('artist');
            const uri = Models.artistURI({ name });
            try {
                const albumArtist = yield artist.get(uri);
                return uri;
            }
            catch (err) {
                if (err.status === 404) {
                    yield artist.put({
                        _id: uri,
                        name
                    });
                    return uri;
                }
            }
        });
    }
    getDisc(disc) {
        if (!disc)
            return 0;
        if (disc.match(/^\d+$/)) {
            return Number(disc);
        }
        const xOnX = disc.match(/^(\d+)\/\d+$/);
        if (xOnX) {
            return Number(xOnX[1]);
        }
        return 0;
    }
    getAlbumYear(file, tags) {
        const date = tags.date || tags.DATE;
        if (!date) {
            const folder = path.basename(path.dirname(file));
            const year = folder.match(/(20(0|1)\d|19\d\d)/);
            if (year !== null && year.length) {
                return Number(year[0]);
            }
        }
        const year = date.match(/(\d\d\d\d)/);
        if (year !== null && year.length) {
            return Number(year[0]);
        }
    }
    databaseEntryCreator(pouchDB, [op, file, entry]) {
        return __awaiter(this, void 0, void 0, function* () {
            switch (op) {
                case 'mkdir':
                    // console.log(`Scanning ${file}`);
                    this.emit('open_folder', file);
                    storyboard_1.mainStory.debug('scanner', `Entering folder ${file}`);
                    return;
                case 'create':
                    this.emit('open_file', file);
                    const source = path.join(this.path, file);
                    try {
                        const probed = yield this.ffprobe(source);
                        storyboard_1.mainStory.debug('scanner', `Probed ${file}`, {
                            attach: probed, attachLevel: 'trace'
                        });
                        if (!probed || !probed.format ||
                            !probed.format.tags || (!probed.format.tags.title && !probed.format.tags.TITLE)) {
                            storyboard_1.mainStory.debug('scanner', `Invalid probed data skipping`);
                            return;
                        }
                        const ext = path.extname(file);
                        const tags = probed.format.tags;
                        const artistName = tags.ALBUM_ARTIST || tags.album_artist || tags.artist || tags.ARTIST;
                        const artistID = Models.artistURI(Models.mapArtistToParams({
                            name: artistName
                        }));
                        const albumName = tags.album || tags.ALBUM;
                        const albumID = Models.albumURI(Models.mapAlbumToParams({
                            name: albumName,
                            artist: artistID
                        }));
                        const trackName = tags.title || tags.TITLE;
                        const trackNumber = (tags.track || tags.TRACK) ?
                            (tags.track || tags.TRACK).match(/^0*(\d+)(\/\d+)?$/)[1] : undefined;
                        const disc = this.getDisc(tags.disc || tags.DISC);
                        const trackID = Models.trackURI(Models.mapTrackToParams({
                            name: trackName,
                            artist: artistID,
                            album: albumID,
                            number: trackNumber,
                            disc: '' + disc
                        }));
                        const fileProps = {
                            _id: '',
                            artist: artistID,
                            album: albumID,
                            number: trackNumber,
                            path: source,
                            bitrate: '' + Math.round(+probed.format.bit_rate / 1000),
                            track: trackID,
                            duration: probed.format.duration,
                            library: this.library,
                            format: ext,
                            hash: ''
                        };
                        const fileID = Models.fileURI(Models.mapFileToParams(fileProps));
                        const docs = {
                            artists: {
                                _id: artistID,
                                name: artistName
                            },
                            albums: {
                                _id: albumID,
                                name: albumName,
                                artist: artistID,
                                year: this.getAlbumYear(source, tags),
                                dateAdded: Date.now()
                            },
                            tracks: {
                                _id: trackID,
                                name: trackName,
                                artist: artistID,
                                track_artist: `${tags.artist | tags.ARTIST}`,
                                album: albumID,
                                number: +trackNumber,
                                disc: disc,
                                duration: +probed.format.duration
                            },
                            files: Object.assign({}, fileProps, {
                                _id: fileID,
                                hash: Models.fileURI(fileID).hash
                            })
                        };
                        storyboard_1.mainStory.trace('scanner', `Creating databse documents`, {
                            attach: docs, attachLevel: 'trace'
                        });
                        const databaseOperations = Object.keys(docs).map((model) => {
                            const props = docs[model];
                            const db = new pouchDB(model);
                            return db.put(props).catch((err) => {
                                if (err.status === 409)
                                    return;
                                storyboard_1.mainStory.warn('scanner', err.message, { attach: err });
                            });
                        });
                        yield Promise.all(databaseOperations);
                    }
                    catch (err) {
                        storyboard_1.mainStory.error('scanner', err.message);
                    }
            }
        });
    }
}
exports.Scanner = Scanner;
//# sourceMappingURL=Scanner.js.map