MediaSyncService::__construct()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 20
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 10
CRAP Score 1

Importance

Changes 0
Metric Value
eloc 9
c 0
b 0
f 0
dl 0
loc 20
ccs 10
cts 10
cp 1
rs 9.9666
cc 1
nc 1
nop 9
crap 1

How to fix   Many Parameters   

Many Parameters

Methods with many parameters are not only hard to understand, but their parameters also often become inconsistent when you need more, or different data.

There are several approaches to avoid long parameter lists:

1
<?php
2
3
namespace App\Services;
4
5
use App\Console\Commands\SyncCommand;
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\Setting;
11
use App\Models\Song;
12
use App\Repositories\AlbumRepository;
13
use App\Repositories\ArtistRepository;
14
use App\Repositories\SettingRepository;
15
use App\Repositories\SongRepository;
16
use Exception;
17
use Psr\Log\LoggerInterface;
18
use SplFileInfo;
19
use Symfony\Component\Finder\Finder;
20
21
class MediaSyncService
22
{
23
    /**
24
     * All applicable tags in a media file that we cater for.
25
     * Note that each isn't necessarily a valid ID3 tag name.
26
     *
27
     * @var array
28
     */
29
    private const APPLICABLE_TAGS = [
30
        'artist',
31
        'album',
32
        'title',
33
        'length',
34
        'track',
35
        'disc',
36
        'lyrics',
37
        'cover',
38
        'mtime',
39
        'compilation',
40
    ];
41
42
    private $mediaMetadataService;
43
    private $songRepository;
44
    private $helperService;
45
    private $fileSynchronizer;
46
    private $finder;
47
    private $artistRepository;
48
    private $albumRepository;
49
    private $settingRepository;
50
    private $logger;
51
52 132
    public function __construct(
53
        MediaMetadataService $mediaMetadataService,
54
        SongRepository $songRepository,
55
        ArtistRepository $artistRepository,
56
        AlbumRepository $albumRepository,
57
        SettingRepository $settingRepository,
58
        HelperService $helperService,
59
        FileSynchronizer $fileSynchronizer,
60
        Finder $finder,
61
        LoggerInterface $logger
62
    ) {
63 132
        $this->mediaMetadataService = $mediaMetadataService;
64 132
        $this->songRepository = $songRepository;
65 132
        $this->helperService = $helperService;
66 132
        $this->fileSynchronizer = $fileSynchronizer;
67 132
        $this->finder = $finder;
68 132
        $this->artistRepository = $artistRepository;
69 132
        $this->albumRepository = $albumRepository;
70 132
        $this->settingRepository = $settingRepository;
71 132
        $this->logger = $logger;
72 132
    }
73
74
    /**
75
     * Tags to be synced.
76
     *
77
     * @var array
78
     */
79
    protected $tags = [];
80
81
    /**
82
     * Sync the media. Oh sync the media.
83
     *
84
     * @param string[]    $tags        The tags to sync.
85
     *                                 Only taken into account for existing records.
86
     *                                 New records will have all tags synced in regardless.
87
     * @param bool        $force       Whether to force syncing even unchanged files
88
     * @param SyncCommand $syncCommand The SyncMedia command object, to log to console if executed by artisan.
89
     *
90
     * @throws Exception
91
     */
92 6
    public function sync(
93
        ?string $mediaPath = null,
94
        array $tags = [],
95
        bool $force = false,
96
        ?SyncCommand $syncCommand = null
97
    ): void {
98 6
        $this->setSystemRequirements();
99 6
        $this->setTags($tags);
100
101
        $results = [
102 6
            'success' => [],
103
            'bad_files' => [],
104
            'unmodified' => [],
105
        ];
106
107 6
        $songPaths = $this->gatherFiles($mediaPath ?: Setting::get('media_path'));
108
109 6
        if ($syncCommand) {
110
            $syncCommand->createProgressBar(count($songPaths));
111
        }
112
113 6
        foreach ($songPaths as $path) {
114 6
            $result = $this->fileSynchronizer->setFile($path)->sync($this->tags, $force);
115
116
            switch ($result) {
117 6
                case FileSynchronizer::SYNC_RESULT_SUCCESS:
118 6
                    $results['success'][] = $path;
119 6
                    break;
120 6
                case FileSynchronizer::SYNC_RESULT_UNMODIFIED:
121 3
                    $results['unmodified'][] = $path;
122 3
                    break;
123
                default:
124 6
                    $results['bad_files'][] = $path;
125 6
                    break;
126
            }
127
128 6
            if ($syncCommand) {
129
                $syncCommand->advanceProgressBar();
130
                $syncCommand->logSyncStatusToConsole($path, $result, $this->fileSynchronizer->getSyncError());
0 ignored issues
show
Bug introduced by
Are you sure the usage of $this->fileSynchronizer->getSyncError() targeting App\Services\FileSynchronizer::getSyncError() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
131
            }
132
        }
133
134
        // Delete non-existing songs.
135
        $hashes = array_map(function (string $path): string {
136 6
            return $this->helperService->getFileHash($path);
137 6
        }, array_merge($results['unmodified'], $results['success']));
138
139 6
        Song::deleteWhereIDsNotIn($hashes);
140
141
        // Trigger LibraryChanged, so that TidyLibrary handler is fired to, erm, tidy our library.
142 6
        event(new LibraryChanged());
143 6
    }
144
145
    /**
146
     * Gather all applicable files in a given directory.
147
     *
148
     * @param string $path The directory's full path
149
     *
150
     * @return SplFileInfo[]
151
     */
152 6
    public function gatherFiles(string $path): array
153
    {
154 6
        return iterator_to_array(
155 6
            $this->finder->create()
156 6
                ->ignoreUnreadableDirs()
157 6
                ->ignoreDotFiles((bool) config('koel.ignore_dot_files')) // https://github.com/phanan/koel/issues/450
158 6
                ->files()
159 6
                ->followLinks()
160 6
                ->name('/\.(mp3|ogg|m4a|flac)$/i')
161 6
                ->in($path)
162
        );
163
    }
164
165
    /**
166
     * Sync media using a watch record.
167
     *
168
     * @throws Exception
169
     */
170 3
    public function syncByWatchRecord(WatchRecordInterface $record): void
171
    {
172 3
        $this->logger->info("New watch record received: '{$record->getPath()}'");
173 3
        $record->isFile() ? $this->syncFileRecord($record) : $this->syncDirectoryRecord($record);
174 3
    }
175
176
    /**
177
     * Sync a file's watch record.
178
     *
179
     * @throws Exception
180
     */
181 2
    private function syncFileRecord(WatchRecordInterface $record): void
182
    {
183 2
        $path = $record->getPath();
184 2
        $this->logger->info("'$path' is a file.");
185
186
        // If the file has been deleted...
187 2
        if ($record->isDeleted()) {
188 1
            $this->handleDeletedFileRecord($path);
189
        }
190
        // Otherwise, it's a new or changed file. Try to sync it in.
191 1
        elseif ($record->isNewOrModified()) {
192 1
            $this->handleNewOrModifiedFileRecord($path);
193
        }
194 2
    }
195
196
    /**
197
     * Sync a directory's watch record.
198
     */
199 1
    private function syncDirectoryRecord(WatchRecordInterface $record): void
200
    {
201 1
        $path = $record->getPath();
202 1
        $this->logger->info("'$path' is a directory.");
203
204 1
        if ($record->isDeleted()) {
205 1
            $this->handleDeletedDirectoryRecord($path);
206
        } elseif ($record->isNewOrModified()) {
207
            $this->handleNewOrModifiedDirectoryRecord($path);
208
        }
209 1
    }
210
211
    /**
212
     * Construct an array of tags to be synced into the database from an input array of tags.
213
     * If the input array is empty or contains only invalid items, we use all tags.
214
     * Otherwise, we only use the valid items in it.
215
     *
216
     * @param string[] $tags
217
     */
218 6
    public function setTags(array $tags = []): void
219
    {
220 6
        $this->tags = array_intersect($tags, self::APPLICABLE_TAGS) ?: self::APPLICABLE_TAGS;
221
222
        // We always keep track of mtime.
223 6
        if (!in_array('mtime', $this->tags, true)) {
224 2
            $this->tags[] = 'mtime';
225
        }
226 6
    }
227
228
    /**
229
     * Tidy up the library by deleting empty albums and artists.
230
     *
231
     * @throws Exception
232
     */
233 7
    public function tidy(): void
234
    {
235 7
        $inUseAlbums = $this->albumRepository->getNonEmptyAlbumIds();
236 7
        $inUseAlbums[] = Album::UNKNOWN_ID;
237 7
        Album::deleteWhereIDsNotIn($inUseAlbums);
238
239 7
        $inUseArtists = $this->artistRepository->getNonEmptyArtistIds();
240 7
        $inUseArtists[] = Artist::UNKNOWN_ID;
241 7
        $inUseArtists[] = Artist::VARIOUS_ID;
242 7
        Artist::deleteWhereIDsNotIn(array_filter($inUseArtists));
243 7
    }
244
245 6
    private function setSystemRequirements(): void
246
    {
247 6
        if (!app()->runningInConsole()) {
0 ignored issues
show
introduced by
The method runningInConsole() does not exist on Illuminate\Container\Container. Are you sure you never get this type here, but always one of the subclasses? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

247
        if (!app()->/** @scrutinizer ignore-call */ runningInConsole()) {
Loading history...
248
            set_time_limit(config('koel.sync.timeout'));
249
        }
250
251 6
        if (config('koel.memory_limit')) {
252
            ini_set('memory_limit', config('koel.memory_limit').'M');
253
        }
254 6
    }
255
256
    /**
257
     * @throws Exception
258
     */
259 1
    private function handleDeletedFileRecord(string $path): void
260
    {
261 1
        if ($song = $this->songRepository->getOneByPath($path)) {
262 1
            $song->delete();
263 1
            $this->logger->info("$path deleted.");
264
265 1
            event(new LibraryChanged());
266
        } else {
267
            $this->logger->info("$path doesn't exist in our database--skipping.");
268
        }
269 1
    }
270
271 1
    private function handleNewOrModifiedFileRecord(string $path): void
272
    {
273 1
        $result = $this->fileSynchronizer->setFile($path)->sync($this->tags);
274
275 1
        if ($result === FileSynchronizer::SYNC_RESULT_SUCCESS) {
276 1
            $this->logger->info("Synchronized $path");
277
        } else {
278
            $this->logger->info("Failed to synchronized $path. Maybe an invalid file?");
279
        }
280
281 1
        event(new LibraryChanged());
282 1
    }
283
284 1
    private function handleDeletedDirectoryRecord(string $path): void
285
    {
286 1
        if ($count = Song::inDirectory($path)->delete()) {
287 1
            $this->logger->info("Deleted $count song(s) under $path");
288
289 1
            event(new LibraryChanged());
290
        } else {
291
            $this->logger->info("$path is empty--no action needed.");
292
        }
293 1
    }
294
295
    private function handleNewOrModifiedDirectoryRecord(string $path): void
296
    {
297
        foreach ($this->gatherFiles($path) as $file) {
298
            $this->fileSynchronizer->setFile($file)->sync($this->tags);
299
        }
300
301
        $this->logger->info("Synced all song(s) under $path");
302
303
        event(new LibraryChanged());
304
    }
305
}
306