org.yamj.core.database.service.CommonStorageService.java Source code

Java tutorial

Introduction

Here is the source code for org.yamj.core.database.service.CommonStorageService.java

Source

/*
 *      Copyright (c) 2004-2015 YAMJ Members
 *      https://github.com/organizations/YAMJ/teams
 *
 *      This file is part of the Yet Another Media Jukebox (YAMJ).
 *
 *      YAMJ is free software: you can redistribute it and/or modify
 *      it under the terms of the GNU General Public License as published by
 *      the Free Software Foundation, either version 3 of the License, or
 *      any later version.
 *
 *      YAMJ is distributed in the hope that it will be useful,
 *      but WITHOUT ANY WARRANTY; without even the implied warranty of
 *      MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *      GNU General Public License for more details.
 *
 *      You should have received a copy of the GNU General Public License
 *      along with YAMJ.  If not, see <http://www.gnu.org/licenses/>.
 *
 *      Web: https://github.com/YAMJ/yamj-v3
 *
 */
package org.yamj.core.database.service;

import java.util.*;
import java.util.Map.Entry;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.io.FilenameUtils;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.yamj.common.type.StatusType;
import org.yamj.core.database.dao.StagingDao;
import org.yamj.core.database.model.*;
import org.yamj.core.database.model.type.ArtworkType;
import org.yamj.core.database.model.type.FileType;
import org.yamj.core.service.file.FileStorageService;
import org.yamj.core.service.file.StorageType;
import org.yamj.core.service.staging.StagingService;
import org.yamj.core.tools.MetadataTools;

@Service("commonStorageService")
public class CommonStorageService {

    private static final Logger LOG = LoggerFactory.getLogger(CommonStorageService.class);

    @Autowired
    private StagingDao stagingDao;
    @Autowired
    private FileStorageService fileStorageService;
    @Autowired
    private StagingService stagingService;

    @Transactional(readOnly = true)
    public List<Long> getStageFilesToDelete() {
        final StringBuilder sb = new StringBuilder();
        sb.append("SELECT f.id FROM StageFile f ");
        sb.append("WHERE f.status = :delete ");

        Map<String, Object> params = Collections.singletonMap("delete", (Object) StatusType.DELETED);
        return stagingDao.findByNamedParameters(sb, params);
    }

    /**
     * Delete a stage file and all associated entities.
     *
     * @param id
     * @return list of cached file names which must be deleted also
     */
    @Transactional
    public Set<String> deleteStageFile(Long id) {
        // get the stage file
        StageFile stageFile = this.stagingDao.getStageFile(id);

        // check stage file
        if (stageFile == null) {
            // not found
            return Collections.emptySet();
        }
        if (StatusType.DELETED != stageFile.getStatus()) {
            // status must still be DELETE
            return Collections.emptySet();
        }

        Set<String> filesToDelete;
        switch (stageFile.getFileType()) {
        case VIDEO:
            filesToDelete = this.deleteVideoStageFile(stageFile);
            break;
        case IMAGE:
            filesToDelete = this.deleteImageStageFile(stageFile);
            break;
        case WATCHED:
            this.deleteWatchedStageFile(stageFile);
            filesToDelete = Collections.emptySet();
            break;
        default:
            this.delete(stageFile);
            filesToDelete = Collections.emptySet();
            break;
        }
        return filesToDelete;
    }

    private Set<String> deleteVideoStageFile(StageFile stageFile) {
        Set<String> filesToDelete = new HashSet<>();

        // delete the media file if no other stage files are present
        MediaFile mediaFile = stageFile.getMediaFile();
        if (mediaFile != null) {
            mediaFile.getStageFiles().remove(stageFile);
            if (CollectionUtils.isEmpty(mediaFile.getStageFiles())) {
                this.delete(mediaFile, filesToDelete);
            } else {
                // mark first duplicate as DONE and reset media file status
                for (StageFile check : mediaFile.getStageFiles()) {
                    if (StatusType.DUPLICATE.equals(check.getStatus())) {
                        check.setStatus(StatusType.DONE);
                        this.stagingDao.updateEntity(check);

                        // reset watched file
                        boolean watchedFile = this.stagingService.isWatchedVideoFile(check);
                        mediaFile.setWatchedFile(watchedFile);
                        mediaFile.setStatus(StatusType.UPDATED);
                        this.stagingDao.updateEntity(mediaFile);

                        for (VideoData videoData : mediaFile.getVideoDatas()) {
                            watchedFile = MetadataTools.allMediaFilesWatched(videoData, false);
                            if (videoData.isWatchedFile() != watchedFile) {
                                videoData.setWatchedFile(watchedFile);
                                this.stagingDao.updateEntity(videoData);
                            }

                            boolean watchedApi = MetadataTools.allMediaFilesWatched(videoData, true);
                            if (videoData.isWatchedApi() != watchedApi) {
                                videoData.setWatchedApi(watchedFile);
                                this.stagingDao.updateEntity(videoData);
                            }
                        }
                        // break the loop; so that just one duplicate is processed
                        break;
                    }
                }
            }

        }

        // delete the stage file
        this.delete(stageFile);

        return filesToDelete;
    }

    private Set<String> deleteImageStageFile(StageFile stageFile) {
        Set<String> filesToDelete = new HashSet<>();

        for (ArtworkLocated located : stageFile.getArtworkLocated()) {
            Artwork artwork = located.getArtwork();
            artwork.getArtworkLocated().remove(located);
            this.delete(artwork, located, filesToDelete);

            // if no located artwork exists anymore then set status of artwork to NEW
            if (CollectionUtils.isEmpty(located.getArtwork().getArtworkLocated())) {
                artwork.setStatus(StatusType.NEW);
                this.stagingDao.updateEntity(artwork);
            }
        }

        // delete stage file
        this.delete(stageFile);

        return filesToDelete;
    }

    private void deleteWatchedStageFile(StageFile watchedFile) {
        // set watched status for video file(s)
        for (StageFile videoFile : this.stagingService.findWatchedVideoFiles(watchedFile)) {
            this.toogleWatchedStatus(videoFile, false, false);
        }

        // delete watched file
        this.delete(watchedFile);
    }

    private void delete(StageFile stageFile) {
        LOG.debug("Delete: {}", stageFile);

        StageDirectory stageDirectory = stageFile.getStageDirectory();

        // just delete the stage file
        this.stagingDao.deleteEntity(stageFile);
        stageDirectory.getStageFiles().remove(stageFile);

        // check and delete stage directory
        this.delete(stageDirectory);
    }

    /**
     * Delete stage file recursivly.
     *
     * @param stageDirectory
     */
    private void delete(StageDirectory stageDirectory) {
        if (stageDirectory == null) {
            return;
        } else if (CollectionUtils.isNotEmpty(stageDirectory.getStageFiles())) {
            return;
        } else if (CollectionUtils.isNotEmpty(this.stagingDao.getChildDirectories(stageDirectory))) {
            return;
        }

        LOG.debug("Delete empty directory:  {}", stageDirectory);
        this.stagingDao.deleteEntity(stageDirectory);

        // recurse to parents
        this.delete(stageDirectory.getParentDirectory());
    }

    private void delete(MediaFile mediaFile, Set<String> filesToDelete) {
        LOG.debug("Delete: {}", mediaFile);

        // delete video data if this is the only media file
        for (VideoData videoData : mediaFile.getVideoDatas()) {
            if (videoData.getMediaFiles().size() == 1) {
                // video data has only this media file
                this.delete(videoData, filesToDelete);
            } else if (videoData.getMediaFiles().size() > 1) {
                // reset watched flag on video data
                videoData.getMediaFiles().remove(mediaFile);

                boolean watchedFile = MetadataTools.allMediaFilesWatched(videoData, false);
                if (videoData.isWatchedFile() != watchedFile) {
                    videoData.setWatchedFile(watchedFile);
                    this.stagingDao.updateEntity(videoData);
                }

                boolean watchedApi = MetadataTools.allMediaFilesWatched(videoData, true);
                if (videoData.isWatchedApi() != watchedApi) {
                    videoData.setWatchedApi(watchedFile);
                    this.stagingDao.updateEntity(videoData);
                }
            }
        }

        // delete media file
        mediaFile.getVideoDatas().clear();
        this.stagingDao.deleteEntity(mediaFile);
    }

    private void delete(VideoData videoData, Set<String> filesToDelete) {
        LOG.debug("Delete: {}", videoData);

        Season season = videoData.getSeason();
        if (season != null) {
            season.getVideoDatas().remove(videoData);
            if (CollectionUtils.isEmpty(season.getVideoDatas())) {
                this.delete(season, filesToDelete);
            }
        }

        // delete artwork
        for (Artwork artwork : videoData.getArtworks()) {
            this.delete(artwork, filesToDelete);
        }

        this.stagingDao.deleteEntity(videoData);
    }

    private void delete(Season season, Set<String> filesToDelete) {
        LOG.debug("Delete: {}", season);

        Series series = season.getSeries();
        series.getSeasons().remove(season);
        if (CollectionUtils.isEmpty(season.getVideoDatas())) {
            this.delete(series, filesToDelete);
        }

        // delete artwork
        for (Artwork artwork : season.getArtworks()) {
            this.delete(artwork, filesToDelete);
        }

        this.stagingDao.deleteEntity(season);
    }

    private void delete(Series series, Set<String> filesToDelete) {
        LOG.debug("Delete: {}", series);

        // delete artwork
        for (Artwork artwork : series.getArtworks()) {
            this.delete(artwork, filesToDelete);
        }

        this.stagingDao.deleteEntity(series);
    }

    private void delete(Artwork artwork, Set<String> filesToDelete) {
        if (artwork == null) {
            return;
        }

        for (ArtworkLocated located : artwork.getArtworkLocated()) {
            this.delete(artwork, located, filesToDelete);
        }

        this.stagingDao.deleteEntity(artwork);
    }

    private void delete(Artwork artwork, ArtworkLocated located, Set<String> filesToDelete) {
        StorageType storageType;
        if (artwork.getArtworkType() == ArtworkType.PHOTO) {
            storageType = StorageType.PHOTO;
        } else {
            storageType = StorageType.ARTWORK;
        }

        // delete generated files
        for (ArtworkGenerated generated : located.getGeneratedArtworks()) {
            this.delete(generated, storageType, filesToDelete);
        }

        // delete located file
        if (StringUtils.isNotBlank(located.getCacheFilename())) {
            String filename = FilenameUtils.concat(located.getCacheDirectory(), located.getCacheFilename());
            filesToDelete.add(this.fileStorageService.getStorageDir(storageType, filename));
        }

        this.stagingDao.deleteEntity(located);
    }

    private void delete(ArtworkGenerated generated, StorageType storageType, Set<String> filesToDelete) {
        String filename = FilenameUtils.concat(generated.getCacheDirectory(), generated.getCacheFilename());
        filesToDelete.add(this.fileStorageService.getStorageDir(storageType, filename));
        this.stagingDao.deleteEntity(generated);
    }

    @Transactional(readOnly = true)
    public List<Long> getOrphanPersons() {
        final StringBuilder query = new StringBuilder();
        query.append("SELECT p.id FROM Person p ");
        query.append("WHERE not exists (select 1 from CastCrew c where c.castCrewPK.person=p)");
        return this.stagingDao.find(query);
    }

    @Transactional
    public Set<String> deletePerson(Long id) {
        Set<String> filesToDelete = new HashSet<>();
        Person person = this.stagingDao.getById(Person.class, id);

        LOG.debug("Delete: {}", person);

        if (person != null) {
            this.delete(person.getPhoto(), filesToDelete);
            this.stagingDao.deleteEntity(person);
        }

        return filesToDelete;
    }

    @Transactional(readOnly = true)
    public List<Long> getOrphanBoxedSets() {
        final StringBuilder query = new StringBuilder();
        query.append("SELECT b.id FROM BoxedSet b ");
        query.append("WHERE not exists (select 1 from BoxedSetOrder o where o.boxedSet=b)");
        return this.stagingDao.find(query);
    }

    @Transactional
    public Set<String> deleteBoxedSet(Long id) {
        Set<String> filesToDelete = new HashSet<>();
        BoxedSet boxedSet = this.stagingDao.getById(BoxedSet.class, id);

        LOG.debug("Delete: {}", boxedSet);

        if (boxedSet != null) {
            // delete artwork
            for (Artwork artwork : boxedSet.getArtworks()) {
                this.delete(artwork, filesToDelete);
            }

            this.stagingDao.deleteEntity(boxedSet);
        }

        return filesToDelete;
    }

    @Transactional
    public Set<String> ignoreArtworkLocated(Long id) {
        ArtworkLocated located = this.stagingDao.getById(ArtworkLocated.class, id);
        if (located != null) {
            StorageType storageType;
            if (located.getArtwork().getArtworkType() == ArtworkType.PHOTO) {
                storageType = StorageType.PHOTO;
            } else {
                storageType = StorageType.ARTWORK;
            }

            Set<String> filesToDelete = new HashSet<>();
            // delete generated files
            for (ArtworkGenerated generated : located.getGeneratedArtworks()) {
                this.delete(generated, storageType, filesToDelete);
            }

            located.getGeneratedArtworks().clear();
            located.setStatus(StatusType.IGNORE);
            stagingDao.updateEntity(located);
            return filesToDelete;
        }
        return null;
    }

    @Transactional
    public boolean toogleWatchedStatus(long id, boolean watched, boolean apiCall) {
        StageFile stageFile = this.stagingDao.getStageFile(id);
        return this.toogleWatchedStatus(stageFile, watched, apiCall);
    }

    @Transactional
    public boolean toogleWatchedStatus(StageFile stageFile, boolean watched, boolean apiCall) {
        if (stageFile == null) {
            return false;
        }
        if (!FileType.VIDEO.equals(stageFile.getFileType())) {
            return false;
        }
        if (StatusType.DUPLICATE.equals(stageFile.getStatus())) {
            return false;
        }
        return this.toggleWatchedStatus(stageFile.getMediaFile(), watched, apiCall);
    }

    @Transactional
    public boolean toggleWatchedStatus(MediaFile mediaFile, boolean watched, boolean apiCall) {
        if (mediaFile == null) {
            return false;
        }

        // update media file
        if (apiCall) {
            mediaFile.setWatchedApi(watched);
        } else {
            mediaFile.setWatchedFile(watched);
        }

        LOG.debug("Mark media file as {} {}: {}", (apiCall ? "api" : "file"), (watched ? "watched" : "unwatched"),
                mediaFile);
        this.stagingDao.updateEntity(mediaFile);

        if (mediaFile.isExtra()) {
            LOG.trace("Media file is an extra where no watched status will be populated: {}", mediaFile);
            return true;
        }

        // determine watch status for each video data which is not an extra
        for (VideoData videoData : mediaFile.getVideoDatas()) {
            boolean watchedAll = MetadataTools.allMediaFilesWatched(videoData, apiCall);
            if (apiCall) {
                if (videoData.isWatchedApi() != watchedAll) {
                    videoData.setWatchedApi(watchedAll);
                    LOG.debug("Mark video as api {}: {}", (watchedAll ? "watched" : "unwatched"), videoData);
                    this.stagingDao.updateEntity(videoData);
                }
            } else if (videoData.isWatchedFile() != watchedAll) {
                videoData.setWatchedFile(watchedAll);
                LOG.debug("Mark video as file {}: {}", (watchedAll ? "watched" : "unwatched"), videoData);
                this.stagingDao.updateEntity(videoData);
            }
        }
        return true;
    }

    @Transactional
    public void updateGenresXml(Map<String, String> subGenres) {
        StringBuilder sb = new StringBuilder();
        sb.append("UPDATE Genre ");
        sb.append("SET targetXml = null ");
        sb.append("WHERE targetXml is not null ");
        sb.append("AND lower(name) not in (:subGenres) ");

        Map<String, Object> params = new HashMap<>();
        params.put("subGenres", subGenres.keySet());
        this.stagingDao.executeUpdate(sb, params);

        for (Entry<String, String> entry : subGenres.entrySet()) {
            sb.setLength(0);
            sb.append("UPDATE Genre ");
            sb.append("SET targetXml=:targetXml ");
            sb.append("WHERE lower(name)=:subGenre ");

            params.clear();
            params.put("subGenre", entry.getKey());
            params.put("targetXml", entry.getValue());
            this.stagingDao.executeUpdate(sb, params);
        }
    }

    @Transactional
    public void updateCountriesXml(Map<String, String> subCountries) {
        StringBuilder sb = new StringBuilder();
        sb.append("UPDATE Country ");
        sb.append("SET targetXml = null ");
        sb.append("WHERE targetXml is not null ");
        sb.append("AND lower(name) not in (:subCountries) ");

        Map<String, Object> params = new HashMap<>();
        params.put("subCountries", subCountries.keySet());
        this.stagingDao.executeUpdate(sb, params);

        for (Entry<String, String> entry : subCountries.entrySet()) {
            sb.setLength(0);
            sb.append("UPDATE Country ");
            sb.append("SET targetXml=:targetXml ");
            sb.append("WHERE lower(name)=:subCountry ");

            params.clear();
            params.put("subCountry", entry.getKey());
            params.put("targetXml", entry.getValue());
            this.stagingDao.executeUpdate(sb, params);
        }
    }

    @Transactional
    public int deleteOrphanGenres() {
        StringBuilder sb = new StringBuilder();
        sb.append("DELETE FROM genre ");
        sb.append("WHERE not exists (select 1 from videodata_genres vg where vg.genre_id=id) ");
        sb.append("AND not exists (select 1 from series_genres sg where sg.genre_id=id) ");
        return this.stagingDao.executeSqlUpdate(sb);
    }

    @Transactional
    public int deleteOrphanStudios() {
        StringBuilder sb = new StringBuilder();
        sb.append("DELETE FROM studio ");
        sb.append("WHERE not exists (select 1 from videodata_studios vs where vs.studio_id=id) ");
        sb.append("AND not exists (select 1 from series_studios ss where ss.studio_id=id) ");
        return this.stagingDao.executeSqlUpdate(sb);
    }

    @Transactional
    public int deleteOrphanCountries() {
        StringBuilder sb = new StringBuilder();
        sb.append("DELETE FROM country ");
        sb.append("WHERE not exists (select 1 from videodata_countries vc where vc.country_id=id) ");
        sb.append("AND not exists (select 1 from series_countries sc where sc.country_id=id) ");
        return this.stagingDao.executeSqlUpdate(sb);
    }

    @Transactional
    public int deleteOrphanCertifications() {
        StringBuilder sb = new StringBuilder();
        sb.append("DELETE FROM certification ");
        sb.append("WHERE not exists (select 1 from videodata_certifications vc where vc.cert_id=id) ");
        sb.append("AND not exists (select 1 from series_certifications sc where sc.cert_id=id) ");
        return this.stagingDao.executeSqlUpdate(sb);
    }
}