Id3v1::__construct()   B
last analyzed

Complexity

Conditions 10
Paths 40

Size

Total Lines 33
Code Lines 23

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 23
dl 0
loc 33
rs 7.6666
c 0
b 0
f 0
cc 10
nc 40
nop 2

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php declare(strict_types=1);
2
3
namespace XoopsModules\Suico;
4
5
/*
6
    Id3v1 - Class for manipulating Id3v1 tags
7
    Copyright (C) 2007  Karol Babioch
8
9
    This program is free software; you can
10
    redistribute it  and/or modify it under the terms
11
    of the GNU General Public License as published by
12
    the Free Software Foundation; either version 2 of
13
    the License, or (at your option) any later version.
14
15
    This program is distributed in the hope that it
16
    will be useful, but WITHOUT ANY WARRANTY; without
17
    even the implied warranty of MERCHANTABILITY or
18
    ITNESS FOR A PARTICULAR PURPOSE. See the GNU
19
    General Public License for more details.
20
21
    You should have received a copy of the GNU
22
    General Public License along with this program;
23
    if not, write to the Free Software Foundation,
24
    Inc., 51 Franklin St, Fifth Floor, Boston,
25
    MA 02110, USA
26
*/
27
28
use RuntimeException;
29
30
/**
31
 * @author      Karol Babioch <[email protected]>
32
 * @copyright   Karol Babioch
33
 * @license     https://www.gnu.org/licenses/gpl-3.0.html GNU GPL
34
 */
35
36
/**
37
 * This class offers you the posibility to manipulate Id3v1 tags
38
 * in a modern, object oriented, way.
39
 *
40
 * This class offers you to read and write Id3v1 in the version
41
 * 1.0 and 1.1. It implements a so called "fluent interface", so
42
 * the access is easy and effective.
43
 *
44
 * @version      1.0
45
 * @link         https://www.babioch.de/
46
 * @author       Karol Babioch <[email protected]>
47
 * @copyright    Karol Babioch
48
 * @license      https://www.gnu.org/licenses/gpl-3.0.html GNU GPL
49
 */
50
class Id3v1
51
{
52
    /**
53
     * Represents Id3v1.0
54
     */
55
    public const ID3V1_0 = 'ID3V1_0';
56
    /**
57
     * Represents Id3v1.1
58
     */
59
    public const ID3V1_1 = 'ID3V1_1';
60
    /**
61
     * Holds the tags
62
     *
63
     * @see __construct
64
     * @see getTitle()
65
     * @see getArtist()
66
     * @see getAlbum()
67
     * @see getYear()
68
     * @see getComment()
69
     * @see getTrack()
70
     * @see getGenre()
71
     * @see getGenreId()
72
     * @see setTitle()
73
     * @see setArtist()
74
     * @see setAlbum()
75
     * @see setYear()
76
     * @see setComment()
77
     * @see setTrack()
78
     * @see setGenre()
79
     * @see setGenreId()
80
     */
81
    protected $_tags = [];
82
    /**
83
     * Holds the PHP stream
84
     *
85
     * @see __construct()
86
     */
87
    protected $_stream;
88
    /**
89
     * Holds the Id3v1 version
90
     *
91
     * @see self::ID3V1_0
92
     * @see self::ID3V1_1
93
     * @see setId3v1Version()
94
     * @see getId3v1Version()
95
     */
96
    protected ?string $_version;
97
    /**
98
     * Indicates if the source is read-only
99
     *
100
     * @see __construct()
101
     */
102
    protected bool $_readOnly = false;
103
    /**
104
     * Holds all known ID3 Genres
105
     *
106
     * @link https://id3.org/d3v2.3.0
107
     * @see  getGenreList()
108
     * @see  getGenreNameByid()
109
     * @see  getGenreIdByName()
110
     */
111
    protected static array $_genres = [
112
        'Blues',
113
        'Classic Rock',
114
        'Country',
115
        'Dance',
116
        'Disco',
117
        'Funk',
118
        'Grunge',
119
        'Hip-Hop',
120
        'Jazz',
121
        'Metal',
122
        'New Age',
123
        'Oldies',
124
        'Other',
125
        'Pop',
126
        'R&B',
127
        'Rap',
128
        'Reggae',
129
        'Rock',
130
        'Techno',
131
        'Industrial',
132
        'Alternative',
133
        'Ska',
134
        'Death Metal',
135
        'Pranks',
136
        'Soundtrack',
137
        'Euro-Techno',
138
        'Ambient',
139
        'Trip-Hop',
140
        'Vocal',
141
        'Jazz+Funk',
142
        'Fusion',
143
        'Trance',
144
        'Classical',
145
        'Instrumental',
146
        'Acid',
147
        'House',
148
        'Game',
149
        'Sound Clip',
150
        'Gospel',
151
        'Noise',
152
        'Alternative Rock',
153
        'Bass',
154
        'Soul',
155
        'Punk',
156
        'Space',
157
        'Meditative',
158
        'Instrumental Pop',
159
        'Instrumental Rock',
160
        'Ethnic',
161
        'Gothic',
162
        'Darkwave',
163
        'Techno-Industrial',
164
        'Electronic',
165
        'Pop-Folk',
166
        'Eurodance',
167
        'Dream',
168
        'Southern Rock',
169
        'Comedy',
170
        'Cult',
171
        'Gangsta',
172
        'Top 40',
173
        'Christian Rap',
174
        'Pop/Funk',
175
        'Jungle',
176
        'Native US',
177
        'Cabaret',
178
        'New Wave',
179
        'Psychedelic',
180
        'Rave',
181
        'Showtunes',
182
        'Trailer',
183
        'Lo-Fi',
184
        'Tribal',
185
        'Acid Punk',
186
        'Acid Jazz',
187
        'Polka',
188
        'Retro',
189
        'Musical',
190
        'Rock & Roll',
191
        'Hard Rock',
192
        'Folk',
193
        'Folk-Rock',
194
        'National Folk',
195
        'Swing',
196
        'Fast Fusion',
197
        'Bebob',
198
        'Latin',
199
        'Revival',
200
        'Celtic',
201
        'Bluegrass',
202
        'Avantgarde',
203
        'Gothic Rock',
204
        'Progressive Rock',
205
        'Psychedelic Rock',
206
        'Symphonic Rock',
207
        'Slow Rock',
208
        'Big Band',
209
        'Chorus',
210
        'Easy Listening',
211
        'Acoustic',
212
        'Humour',
213
        'Speech',
214
        'Chanson',
215
        'Opera',
216
        'Chamber Music',
217
        'Sonata',
218
        'Symphony',
219
        'Booty Bass',
220
        'Primus',
221
        'Porn Groove',
222
        'Satire',
223
        'Slow Jam',
224
        'Club',
225
        'Tango',
226
        'Samba',
227
        'Folklore',
228
        'Ballad',
229
        'Power Ballad',
230
        'Rhytmic Soul',
231
        'Freestyle',
232
        'Duet',
233
        'Punk Rock',
234
        'Drum Solo',
235
        'Acapella',
236
        'Euro-House',
237
        'Dance Hall',
238
    ];
239
240
    /**
241
     * Class constructor
242
     *
243
     * Checks if the parameters are valid and then gets ID3 tags, if there
244
     * are some.
245
     *
246
     * @param      $filename
247
     * @param bool $readOnly
248
     * @see $_tags
249
     */
250
    public function __construct(
251
        $filename,
252
        $readOnly = false
253
    ) {
254
        if (\is_bool($readOnly)) {
0 ignored issues
show
introduced by
The condition is_bool($readOnly) is always true.
Loading history...
255
            $this->_readOnly = $readOnly;
256
        }
257
        if (!\is_string($filename)) {
258
            throw new RuntimeException('Filename must be a string');
259
        }
260
        if (!\is_file($filename)) {
261
            throw new RuntimeException('File doesn\'t exist');
262
        }
263
        $mode = $this->_readOnly ? 'rb' : 'rb+';
264
        if (!$this->_stream = @\fopen($filename, $mode, false)) {
265
            throw new RuntimeException('File cannot be opened');
266
        }
267
        if (!$this->_readOnly) {
268
            \flock($this->_stream, \LOCK_SH);
269
        }
270
        \fseek($this->_stream, -128, \SEEK_END);
271
        $rawTag = \fread($this->_stream, 128);
272
        if ($rawTag[125] === \chr(0) && $rawTag[126] !== \chr(0)) {
273
            $format         = 'a3marking/a30title/a30artist/a30album/a4year' . '/a28comment/x1/C1track/C1genre';
274
            $this->_version = self::ID3V1_1;
275
        } else {
276
            $format         = 'a3marking/a30title/a30artist/a30album/a4year' . '/a30comment/C1genre';
277
            $this->_version = self::ID3V1_0;
278
        }
279
        $tags = \unpack($format, $rawTag);
280
        $this->clearAllTags();
281
        if ('TAG' === $tags['marking']) {
282
            $this->_tags = $tags;
283
        }
284
    }
285
286
    /**
287
     * Short way to access tags
288
     *
289
     * This method allows a shorter way to access tags.
290
     * You can access the tags like normal properties,
291
     * and don't have to call methods.
292
     *
293
     * Here an example:
294
     * <code>
295
     * <?php
296
     *     // two ways to do the same
297
     *     $id3v1->title;
298
     *     $id3v1->getTitle();
299
     * ?>
300
     * </code>
301
     *
302
     * @param string $name
303
     * @return mixed Depends on tag, which will be returned
304
     * @throws \Exception
305
     */
306
    public function __get(
307
        $name
308
    ) {
309
        $validNames = [
310
            'title',
311
            'artist',
312
            'album',
313
            'year',
314
            'comment',
315
            'track',
316
            'genre',
317
        ];
318
        if (\in_array($name, $validNames, true)) {
319
            return $this->{'get' . \ucfirst($name)}();
320
        }
321
        throw new RuntimeException('Property doesn\'t exist');
322
    }
323
324
    /**
325
     * Short way to assign tags
326
     *
327
     * This method allows a shorter way to assign tags.
328
     * You can assign the tags like normal properties,
329
     * and don't have to call methods.
330
     *
331
     * Here an example:
332
     * <code>
333
     * <?php
334
     *     // two ways to do the same
335
     *     $id3v1->title = 'Something';
336
     *     $id3v1->setTitle('Something);
337
     * ?>
338
     * </code>
339
     *
340
     * @param string $name
341
     * @param string $value
342
     * @return mixed Depends on tag
343
     * @throws \Exception
344
     */
345
    public function __set(
346
        $name,
347
        $value
348
    ) {
349
        $validNames = [
350
            'title',
351
            'artist',
352
            'album',
353
            'year',
354
            'comment',
355
            'track',
356
            'genre',
357
        ];
358
        if (\in_array($name, $validNames, true)) {
359
            return $this->{'set' . \ucfirst($name)}($value);
360
        }
361
        throw new RuntimeException('Property doesn\'t exist');
362
    }
363
364
    /**
365
     * Magic method, which gets called when casting to string
366
     *
367
     * This method will get called when the object will be used
368
     * in a context, which requires a string. You will get a
369
     * short overview over all meaningfully tags.
370
     *
371
     * Here an example:
372
     * <code>
373
     * <?php
374
     *     // casting to string
375
     *     echo $id3v1;
376
     * ?>
377
     * </code>
378
     *
379
     * @return string
380
     */
381
    public function __toString()
382
    {
383
        $returnedTags = [];
384
        foreach ($this->_tags as $tagKey => $tagVal) {
385
            if ('TAG' === $tagVal) {
386
                continue;
387
            }
388
            if (null === $tagVal || !$tagVal) {
389
                continue;
390
            }
391
            if ('genre' === $tagKey) {
392
                $returnedTags[] = self::getGenreNameById($tagVal);
393
                continue;
394
            }
395
            $returnedTags[] = $tagVal;
396
        }
397
        if (\count($returnedTags) > 0) {
398
            return \implode(', ', $returnedTags);
399
        }
400
    }
401
402
    /**
403
     * Gets the title out of the ID3 bytestream
404
     *
405
     * @return string
406
     */
407
    public function getTitle()
408
    {
409
        if (!empty($this->_tags['title'])) {
410
            return $this->_tags['title'];
411
        }
412
413
        return null;
414
    }
415
416
    /**
417
     * Gets the artist out of the ID3 bytestream
418
     *
419
     * @return string
420
     */
421
    public function getArtist()
422
    {
423
        if (!empty($this->_tags['artists'])) {
424
            return $this->_tags['artist'];
425
        }
426
427
        return null;
428
    }
429
430
    /**
431
     * Gets the album out of the ID3 bytestream
432
     *
433
     * @return string
434
     */
435
    public function getAlbum()
436
    {
437
        if (!empty($this->_tags['album'])) {
438
            return $this->_tags['album'];
439
        }
440
441
        return null;
442
    }
443
444
    /**
445
     * Gets the comment out of the ID3 bytestream
446
     *
447
     * @return string
448
     */
449
    public function getComment()
450
    {
451
        if (self::ID3V1_1 === $this->_version) {
452
            return mb_substr($this->_tags['comment'], 0, 28);
453
        }
454
455
        return $this->_tags['comment'];
456
    }
457
458
    /**
459
     * Gets the genre name in dependece of the genre id
460
     *
461
     * @return string
462
     * @uses getGenreNameById()
463
     */
464
    public function getGenre()
465
    {
466
        return self::getGenreNameById($this->_tags['genre']);
0 ignored issues
show
Bug Best Practice introduced by
The expression return self::getGenreNam...($this->_tags['genre']) also could return the type boolean which is incompatible with the documented return type string.
Loading history...
467
    }
468
469
    /**
470
     * Gets the genre id out of the ID3 bytestream
471
     *
472
     * @return int
473
     */
474
    public function getGenreId()
475
    {
476
        return $this->_tags['genre'];
477
    }
478
479
    /**
480
     * Gets the year out of the ID3 bytestream
481
     *
482
     * @return int
483
     */
484
    public function getYear()
485
    {
486
        if (!empty($this->_tags['year'])) {
487
            return (int)$this->_tags['year'];
488
        }
489
490
        return null;
491
    }
492
493
    /**
494
     * Gets the track number out of the ID3 bytestream
495
     *
496
     * @return mixed If there is no track false will be returned, else the track
497
     */
498
    public function getTrack()
499
    {
500
        if (self::ID3V1_0 === $this->_version || !isset($this->_tags['track'])) {
501
            return false;
502
        }
503
504
        return (int)$this->_tags['track'];
505
    }
506
507
    /**
508
     * Gets the Id3v1 version, which is used
509
     *
510
     * @return string
511
     * @see self::ID3V1_1
512
     * @see self::ID3V1_0
513
     */
514
    public function getId3v1Version()
515
    {
516
        return \constant('self::' . $this->_version);
517
    }
518
519
    /**
520
     * Sets the Id3v1 version, which will be used
521
     *
522
     * @param string $version The version you want to set
523
     * @return Id3v1 Implements fluent interface
524
     * @throws \Exception
525
     * @see self::ID3V1_1
526
     * @see self::ID3V1_0
527
     */
528
    public function setId3v1Version(
529
        $version
530
    ) {
531
        if ($this->_readOnly) {
532
            return $this;
533
        }
534
        switch ($version) {
535
            case self::ID3V1_0:
536
            case self::ID3V1_1:
537
                break;
538
            default:
539
                throw new RuntimeException('Invalid version');
540
        }
541
        $this->_version = $version;
542
543
        return $this;
544
    }
545
546
    /**
547
     * Sets the title
548
     *
549
     * The maximum length of this property, which will get stored is 30, even if
550
     * the method itself also accepts longer terms.
551
     *
552
     * @param string $title The title you want to set
553
     * @return Id3v1 Implements fluent interface
554
     * @throws \Exception
555
     * @see $_tags
556
     */
557
    public function setTitle(
558
        $title
559
    ) {
560
        if ($this->_readOnly) {
561
            return $this;
562
        }
563
        if (\is_string($title)) {
0 ignored issues
show
introduced by
The condition is_string($title) is always true.
Loading history...
564
            $this->_tags['title'] = $title;
565
        } else {
566
            throw new RuntimeException('Title has to be a string');
567
        }
568
569
        return $this;
570
    }
571
572
    /**
573
     * Sets the artist
574
     *
575
     * The maximum length of this property, which will get stored is 30, even if
576
     * the method itself also accepts longer terms.
577
     *
578
     * @param string $artist The artist you want to set
579
     * @return Id3v1 Implements fluent interface
580
     * @throws \Exception
581
     * @see $_tags
582
     */
583
    public function setArtist(
584
        $artist
585
    ) {
586
        if ($this->_readOnly) {
587
            return $this;
588
        }
589
        if (\is_string($artist)) {
0 ignored issues
show
introduced by
The condition is_string($artist) is always true.
Loading history...
590
            $this->_tags['artist'] = $artist;
591
        } else {
592
            throw new RuntimeException('Artist has to be a string');
593
        }
594
595
        return $this;
596
    }
597
598
    /**
599
     * Sets the album
600
     *
601
     * The maximum length of this property, which will get stored is 30, even if
602
     * the method itself also accepts longer terms.
603
     *
604
     * @param string $album The album you want to set
605
     * @return Id3v1 Implements fluent interface
606
     * @throws \Exception
607
     * @see $_tags
608
     */
609
    public function setAlbum(
610
        $album
611
    ) {
612
        if ($this->_readOnly) {
613
            return $this;
614
        }
615
        if (\is_string($album)) {
0 ignored issues
show
introduced by
The condition is_string($album) is always true.
Loading history...
616
            $this->_tags['album'] = $album;
617
        } else {
618
            throw new RuntimeException('Album has to be a string');
619
        }
620
621
        return $this;
622
    }
623
624
    /**
625
     * Sets the comment
626
     *
627
     * The maximum length of this property, which will get stored depends
628
     * on the used Id3v1 version. Where ID3V1_0 stores 30, ID3V1_1 just
629
     * stores 28 characters, in order to save place for the track number.
630
     * The method itselfs takes also longer terms.
631
     *
632
     * @param string $comment The comment you want to set
633
     * @return Id3v1 Implements fluent interface
634
     * @throws \Exception
635
     * @see $_tags
636
     */
637
    public function setComment(
638
        $comment
639
    ) {
640
        if ($this->_readOnly) {
641
            return $this;
642
        }
643
        if (\is_string($comment)) {
0 ignored issues
show
introduced by
The condition is_string($comment) is always true.
Loading history...
644
            $this->_tags['comment'] = $comment;
645
        } else {
646
            throw new RuntimeException('Comment has to be a string');
647
        }
648
649
        return $this;
650
    }
651
652
    /**
653
     * Sets the genre
654
     *
655
     * You can either use the genre id, or the genre name.
656
     *
657
     * @param string|int $genre The genre you want to set
658
     * @return Id3v1 Implements fluent interface
659
     * @throws \Exception
660
     * @see $_tags
661
     */
662
    public function setGenre(
663
        $genre
664
    ) {
665
        if ($this->_readOnly) {
666
            return $this;
667
        }
668
        if (\is_int($genre)) {
669
            $this->_tags['genre'] = $genre;
670
        } elseif (\is_string($genre)) {
0 ignored issues
show
introduced by
The condition is_string($genre) is always true.
Loading history...
671
            $this->_tags['genre'] = self::getGenreIdByName($genre);
672
        } else {
673
            throw new RuntimeException('Genre type invalid');
674
        }
675
676
        return $this;
677
    }
678
679
    /**
680
     * Sets the year
681
     *
682
     * @param int $year The year you want to set
683
     * @return Id3v1 Implements fluent interface
684
     * @throws \Exception
685
     * @see $_tags
686
     */
687
    public function setYear(
688
        $year
689
    ) {
690
        if ($this->_readOnly) {
691
            return $this;
692
        }
693
        if (\is_int($year)) {
0 ignored issues
show
introduced by
The condition is_int($year) is always true.
Loading history...
694
            $this->_tags['year'] = $year;
695
        } else {
696
            throw new RuntimeException('Year has to be an interger');
697
        }
698
699
        return $this;
700
    }
701
702
    /**
703
     * Sets the track
704
     *
705
     * The Id3v1 version will be set automatically to ID3V1_1, because
706
     * this property is just defined there. If you want to store explicit
707
     * ID3V1_0 you have to set it manually after calling this method.
708
     *
709
     * @param int $track The tracl you want to set
710
     * @return Id3v1 Implements fluent interface
711
     * @throws \Exception
712
     * @see $_tags
713
     * @see setId3v1Version()
714
     * @see getId3v1Version()
715
     */
716
    public function setTrack(
717
        $track
718
    ) {
719
        if ($this->_readOnly) {
720
            return $this;
721
        }
722
        if (\is_int($track) && 0 !== $track) {
723
            $this->_tags['track'] = $track;
724
            $this->_version       = self::ID3V1_1;
725
        } else {
726
            throw new RuntimeException('Track type invalid or zero');
727
        }
728
729
        return $this;
730
    }
731
732
    /**
733
     * Clears all tags
734
     *
735
     * This method sets the default value for each tag.
736
     *
737
     * @return Id3v1 Implements fluent interface
738
     * @see $_tags
739
     */
740
    public function clearAllTags()
741
    {
742
        if ($this->_readOnly) {
743
            return $this;
744
        }
745
        $this->_tags['marking'] = 'TAG';
746
        $this->_tags['title']   = '';
747
        $this->_tags['artist']  = '';
748
        $this->_tags['album']   = '';
749
        $this->_tags['year']    = null;
750
        $this->_tags['comment'] = '';
751
        $this->_tags['track']   = null;
752
        $this->_tags['genre']   = 255;
753
754
        return $this;
755
    }
756
757
    /**
758
     * Gets the genre id out of a genre name
759
     *
760
     * @param $genreName
761
     * @return int
762
     * @see $_genres
763
     */
764
    public static function getGenreIdByName(
765
        $genreName
766
    ) {
767
        $genres = \array_flip(self::$_genres);
768
        if (!isset($genres[$genreName])) {
769
            return 255;
770
        }
771
772
        return (int)$genres[$genreName];
773
    }
774
775
    /**
776
     * Gets the genre name out of a genre id
777
     *
778
     * @param $genreId
779
     * @return string|bool
780
     * @see $_genres
781
     */
782
    public static function getGenreNameById(
783
        $genreId
784
    ) {
785
        return self::$_genres[$genreId] ?? false;
786
    }
787
788
    /**
789
     * Returns a array with all defined genres
790
     *
791
     * @return array
792
     * @see $_genres
793
     */
794
    public static function getGenreList()
795
    {
796
        return self::$_genres;
797
    }
798
799
    /**
800
     * Saves the set tags to the file
801
     *
802
     * This method saves the set tags to the file. Therefore it seeks to
803
     * the end of the file and writes the Id3v1 bytestream to it.
804
     *
805
     * @return Id3v1 Implements fluent interface
806
     * @throws \Exception
807
     * @see $_tags
808
     * @see setTitle()
809
     * @see setArtist()
810
     * @see setAlbum()
811
     * @see setComment()
812
     * @see setGenre()
813
     * @see setYear()
814
     * @see setTrack()
815
     */
816
    public function save()
817
    {
818
        if ($this->_readOnly) {
819
            return $this;
820
        }
821
        \fseek($this->_stream, -128, \SEEK_END);
822
        if ('TAG' !== $this->_tags['marking']) {
823
            \fseek($this->_stream, 0, \SEEK_END);
824
        }
825
        $newTag = '';
826
        if (self::ID3V1_0 === $this->_version) {
827
            $newTag = \pack(
828
                'a3a30a30a30a4a30C1',
829
                'TAG',
830
                $this->_tags['title'],
831
                $this->_tags['artist'],
832
                $this->_tags['album'],
833
                $this->_tags['year'],
834
                $this->_tags['comment'],
835
                $this->_tags['genre']
836
            );
837
        } else {
838
            $newTag = \pack(
839
                'a3a30a30a30a4a28x1C1C1',
840
                'TAG',
841
                $this->_tags['title'],
842
                $this->_tags['artist'],
843
                $this->_tags['album'],
844
                $this->_tags['year'],
845
                $this->_tags['comment'],
846
                $this->_tags['track'],
847
                $this->_tags['genre']
848
            );
849
        }
850
        if (false === \fwrite($this->_stream, $newTag, 128)) {
851
            throw new RuntimeException('Not possible to write ID3 tags');
852
        }
853
854
        return $this;
855
    }
856
}
857