Test Setup Failed
Push — master ( 0a8e7c...aa7267 )
by Phan
03:42
created

Media::syncByWatchRecord()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 5
rs 9.4285
c 0
b 0
f 0
cc 2
eloc 3
nc 2
nop 1
1
<?php
2
3
namespace App\Services;
4
5
use App\Console\Commands\SyncMedia;
6
use App\Events\LibraryChanged;
7
use App\Libraries\WatchRecord\WatchRecordInterface;
8
use App\Models\Album;
9
use App\Models\Artist;
10
use App\Models\File;
11
use App\Models\Setting;
12
use App\Models\Song;
13
use getID3;
14
use Illuminate\Support\Facades\Log;
15
use Symfony\Component\Finder\Finder;
16
17
class Media
18
{
19
    /**
20
     * All applicable tags in a media file that we cater for.
21
     * Note that each isn't necessarily a valid ID3 tag name.
22
     *
23
     * @var array
24
     */
25
    protected $allTags = [
26
        'artist',
27
        'album',
28
        'title',
29
        'length',
30
        'track',
31
        'lyrics',
32
        'cover',
33
        'mtime',
34
        'compilation',
35
    ];
36
37
    /**
38
     * Tags to be synced.
39
     *
40
     * @var array
41
     */
42
    protected $tags = [];
43
44
    /**
45
     * Sync the media. Oh sync the media.
46
     *
47
     * @param string|null $mediaPath
48
     * @param array       $tags        The tags to sync.
49
     *                                 Only taken into account for existing records.
50
     *                                 New records will have all tags synced in regardless.
51
     * @param bool        $force       Whether to force syncing even unchanged files
52
     * @param SyncMedia   $syncCommand The SyncMedia command object, to log to console if executed by artisan.
53
     */
54
    public function sync($mediaPath = null, $tags = [], $force = false, SyncMedia $syncCommand = null)
55
    {
56
        if (!app()->runningInConsole()) {
57
            set_time_limit(config('koel.sync.timeout'));
58
        }
59
60
        $mediaPath = $mediaPath ?: Setting::get('media_path');
61
        $this->setTags($tags);
62
63
        $results = [
64
            'success' => [],
65
            'bad_files' => [],
66
            'unmodified' => [],
67
        ];
68
69
        $getID3 = new getID3();
70
        $songPaths = $this->gatherFiles($mediaPath);
71
        $syncCommand && $syncCommand->createProgressBar(count($songPaths));
72
73
        foreach ($songPaths as $path) {
74
            $file = new File($path, $getID3);
75
76
            switch ($result = $file->sync($this->tags, $force)) {
77
                case File::SYNC_RESULT_SUCCESS:
78
                    $results['success'][] = $file;
79
                    break;
80
                case File::SYNC_RESULT_UNMODIFIED:
81
                    $results['unmodified'][] = $file;
82
                    break;
83
                default:
84
                    $results['bad_files'][] = $file;
85
                    break;
86
            }
87
88
            if ($syncCommand) {
89
                $syncCommand->updateProgressBar();
90
                $syncCommand->logToConsole($file->getPath(), $result, $file->getSyncError());
91
            }
92
        }
93
94
        // Delete non-existing songs.
95
        $hashes = array_map(function (File $file) {
96
            return self::getHash($file->getPath());
97
        }, array_merge($results['unmodified'], $results['success']));
98
99
        Song::deleteWhereIDsNotIn($hashes);
100
101
        // Trigger LibraryChanged, so that TidyLibrary handler is fired to, erm, tidy our library.
102
        event(new LibraryChanged());
103
    }
104
105
    /**
106
     * Gather all applicable files in a given directory.
107
     *
108
     * @param string $path The directory's full path
109
     *
110
     * @return array An array of SplFileInfo objects
111
     */
112
    public function gatherFiles($path)
113
    {
114
        return iterator_to_array(
115
            Finder::create()
116
                ->ignoreUnreadableDirs()
117
                ->ignoreDotFiles((bool) config('koel.ignore_dot_files')) // https://github.com/phanan/koel/issues/450
118
                ->files()
119
                ->followLinks()
120
                ->name('/\.(mp3|ogg|m4a|flac)$/i')
121
                ->in($path)
122
        );
123
    }
124
125
    /**
126
     * Sync media using a watch record.
127
     *
128
     * @param WatchRecordInterface $record The watch record.
129
     */
130
    public function syncByWatchRecord(WatchRecordInterface $record)
131
    {
132
        Log::info("New watch record received: '$record'");
133
        $record->isFile() ? $this->syncFileRecord($record) : $this->syncDirectoryRecord($record);
134
    }
135
136
    /**
137
     * Sync a file's watch record.
138
     *
139
     * @param WatchRecordInterface $record
140
     */
141
    private function syncFileRecord(WatchRecordInterface $record)
142
    {
143
        $path = $record->getPath();
144
        Log::info("'$path' is a file.");
145
146
        // If the file has been deleted...
147
        if ($record->isDeleted()) {
148
            // ...and it has a record in our database, remove it.
149
            if ($song = Song::byPath($path)) {
150
                $song->delete();
151
                Log::info("$path deleted.");
152
153
                event(new LibraryChanged());
154
            } else {
155
                Log::info("$path doesn't exist in our database--skipping.");
156
            }
157
        }
158
        // Otherwise, it's a new or changed file. Try to sync it in.
159
        // File format etc. will be handled by File::sync().
160
        elseif ($record->isNewOrModified()) {
161
            $result = (new File($path))->sync($this->tags);
162
            Log::info($result === File::SYNC_RESULT_SUCCESS ? "Synchronized $path" : "Invalid file $path");
163
164
            event(new LibraryChanged());
165
        }
166
    }
167
168
    /**
169
     * Sync a directory's watch record.
170
     *
171
     * @param WatchRecordInterface $record
172
     */
173
    private function syncDirectoryRecord(WatchRecordInterface $record)
174
    {
175
        $path = $record->getPath();
176
        Log::info("'$path' is a directory.");
177
178
        if ($record->isDeleted()) {
179
            // The directory is removed. We remove all songs in it.
180
            if ($count = Song::inDirectory($path)->delete()) {
0 ignored issues
show
Bug introduced by
The method inDirectory() does not exist on App\Models\Song. Did you maybe mean scopeInDirectory()?

This check marks calls to methods that do not seem to exist on an object.

This is most likely the result of a method being renamed without all references to it being renamed likewise.

Loading history...
181
                Log::info("Deleted $count song(s) under $path");
182
183
                event(new LibraryChanged());
184
            } else {
185
                Log::info("$path is empty--no action needed.");
186
            }
187
        } elseif ($record->isNewOrModified()) {
188
            foreach ($this->gatherFiles($path) as $file) {
189
                (new File($file))->sync($this->tags);
190
            }
191
            Log::info("Synced all song(s) under $path");
192
193
            event(new LibraryChanged());
194
        }
195
    }
196
197
    /**
198
     * Construct an array of tags to be synced into the database from an input array of tags.
199
     * If the input array is empty or contains only invalid items, we use all tags.
200
     * Otherwise, we only use the valid items in it.
201
     *
202
     * @param array $tags
203
     */
204
    public function setTags($tags = [])
205
    {
206
        $this->tags = array_intersect((array) $tags, $this->allTags) ?: $this->allTags;
207
208
        // We always keep track of mtime.
209
        if (!in_array('mtime', $this->tags, true)) {
210
            $this->tags[] = 'mtime';
211
        }
212
    }
213
214
    /**
215
     * Generate a unique hash for a file path.
216
     *
217
     * @param $path
218
     *
219
     * @return string
220
     */
221
    public function getHash($path)
222
    {
223
        return File::getHash($path);
224
    }
225
226
    /**
227
     * Tidy up the library by deleting empty albums and artists.
228
     */
229
    public function tidy()
230
    {
231
        $inUseAlbums = Song::select('album_id')
232
            ->groupBy('album_id')
233
            ->get()
234
            ->pluck('album_id')
235
            ->toArray();
236
        $inUseAlbums[] = Album::UNKNOWN_ID;
237
        Album::deleteWhereIDsNotIn($inUseAlbums);
238
239
        $inUseArtists = Song::select('artist_id')
240
            ->groupBy('artist_id')
241
            ->get()
242
            ->pluck('artist_id')
243
            ->toArray();
244
        $inUseArtists[] = Artist::UNKNOWN_ID;
245
        $inUseArtists[] = Artist::VARIOUS_ID;
246
        Artist::deleteWhereIDsNotIn(array_filter($inUseArtists));
247
    }
248
}
249