id3taginator 0.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (150) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +13 -0
  3. data/.idea/.gitignore +8 -0
  4. data/.idea/ID3Taginator.iml +73 -0
  5. data/.idea/misc.xml +4 -0
  6. data/.idea/modules.xml +8 -0
  7. data/.idea/vcs.xml +6 -0
  8. data/.rspec +3 -0
  9. data/.rubocop.yml +26 -0
  10. data/CHANGELOG.md +5 -0
  11. data/CODE_OF_CONDUCT.md +84 -0
  12. data/Gemfile +12 -0
  13. data/LICENSE.txt +21 -0
  14. data/README.md +408 -0
  15. data/Rakefile +12 -0
  16. data/bin/console +15 -0
  17. data/bin/setup +8 -0
  18. data/id3taginator.gemspec +37 -0
  19. data/lib/id3taginator/audio_file.rb +136 -0
  20. data/lib/id3taginator/errors/id3_tag_error.rb +12 -0
  21. data/lib/id3taginator/extensions/argument_check.rb +88 -0
  22. data/lib/id3taginator/extensions/comparable.rb +28 -0
  23. data/lib/id3taginator/extensions/encodable.rb +215 -0
  24. data/lib/id3taginator/extensions/optionable.rb +73 -0
  25. data/lib/id3taginator/frames/buffer/entities/buffer.rb +26 -0
  26. data/lib/id3taginator/frames/buffer/rbuf_recommended_buffer_size_frame.rb +59 -0
  27. data/lib/id3taginator/frames/buffer_frames.rb +32 -0
  28. data/lib/id3taginator/frames/comment/comm_comment_frame.rb +56 -0
  29. data/lib/id3taginator/frames/comment/entities/comment.rb +26 -0
  30. data/lib/id3taginator/frames/comment_frames.rb +42 -0
  31. data/lib/id3taginator/frames/count/entities/popularimeter.rb +26 -0
  32. data/lib/id3taginator/frames/count/pcnt_play_counter_frame.rb +40 -0
  33. data/lib/id3taginator/frames/count/popm_popularimeter_frame.rb +52 -0
  34. data/lib/id3taginator/frames/count_frames.rb +51 -0
  35. data/lib/id3taginator/frames/custom_frame.rb +39 -0
  36. data/lib/id3taginator/frames/custom_frames.rb +54 -0
  37. data/lib/id3taginator/frames/encryption/aenc_audio_encryption.rb +54 -0
  38. data/lib/id3taginator/frames/encryption/encr_encryption_method_frame.rb +49 -0
  39. data/lib/id3taginator/frames/encryption/entities/audio_encryption.rb +28 -0
  40. data/lib/id3taginator/frames/encryption/entities/encryption_method.rb +26 -0
  41. data/lib/id3taginator/frames/encryption_frames.rb +77 -0
  42. data/lib/id3taginator/frames/frameable.rb +75 -0
  43. data/lib/id3taginator/frames/geo/entities/encapsulated_object.rb +28 -0
  44. data/lib/id3taginator/frames/geo/geob_general_encapsulated_object_frame.rb +59 -0
  45. data/lib/id3taginator/frames/geo_frames.rb +41 -0
  46. data/lib/id3taginator/frames/grouping/entities/group_identification.rb +26 -0
  47. data/lib/id3taginator/frames/grouping/grid_group_identification_frame.rb +49 -0
  48. data/lib/id3taginator/frames/grouping/grp1_grouping_frame.rb +40 -0
  49. data/lib/id3taginator/frames/grouping_frames.rb +61 -0
  50. data/lib/id3taginator/frames/has_id.rb +51 -0
  51. data/lib/id3taginator/frames/id3v23_frame_flags.rb +64 -0
  52. data/lib/id3taginator/frames/id3v24_frame_flags.rb +80 -0
  53. data/lib/id3taginator/frames/id3v2_frame.rb +249 -0
  54. data/lib/id3taginator/frames/id3v2_frame_factory.rb +98 -0
  55. data/lib/id3taginator/frames/ipl/entities/involved_person.rb +24 -0
  56. data/lib/id3taginator/frames/ipl/ipls_involved_people_frame.rb +48 -0
  57. data/lib/id3taginator/frames/ipl_frames.rb +31 -0
  58. data/lib/id3taginator/frames/lyrics/entities/unsync_lyrics.rb +26 -0
  59. data/lib/id3taginator/frames/lyrics/uslt_unsync_lyrics_frame.rb +56 -0
  60. data/lib/id3taginator/frames/lyrics_frames.rb +42 -0
  61. data/lib/id3taginator/frames/mcdi/mcdi_music_cd_identifier_frame.rb +40 -0
  62. data/lib/id3taginator/frames/mcdi_frames.rb +28 -0
  63. data/lib/id3taginator/frames/picture/apic_picture_frame.rb +106 -0
  64. data/lib/id3taginator/frames/picture/entities/picture.rb +32 -0
  65. data/lib/id3taginator/frames/picture_frames.rb +51 -0
  66. data/lib/id3taginator/frames/private/entities/private_frame.rb +24 -0
  67. data/lib/id3taginator/frames/private/priv_private_frame.rb +45 -0
  68. data/lib/id3taginator/frames/private_frames.rb +40 -0
  69. data/lib/id3taginator/frames/text/entities/copyright.rb +24 -0
  70. data/lib/id3taginator/frames/text/entities/date.rb +24 -0
  71. data/lib/id3taginator/frames/text/entities/part_of_set.rb +24 -0
  72. data/lib/id3taginator/frames/text/entities/time.rb +24 -0
  73. data/lib/id3taginator/frames/text/entities/track_number.rb +24 -0
  74. data/lib/id3taginator/frames/text/entities/user_info.rb +24 -0
  75. data/lib/id3taginator/frames/text/talb_album_frame.rb +40 -0
  76. data/lib/id3taginator/frames/text/tbpm_bpm_frame.rb +40 -0
  77. data/lib/id3taginator/frames/text/tcom_composer_frame.rb +42 -0
  78. data/lib/id3taginator/frames/text/tcon_genre_frame.rb +104 -0
  79. data/lib/id3taginator/frames/text/tcop_copyright_frame.rb +55 -0
  80. data/lib/id3taginator/frames/text/tdat_date_frame.rb +60 -0
  81. data/lib/id3taginator/frames/text/tdly_playlist_delay_frame.rb +40 -0
  82. data/lib/id3taginator/frames/text/tenc_encoded_by_frame.rb +40 -0
  83. data/lib/id3taginator/frames/text/text_writers_frame.rb +43 -0
  84. data/lib/id3taginator/frames/text/tflt_file_type_frame.rb +71 -0
  85. data/lib/id3taginator/frames/text/time_time_frame.rb +60 -0
  86. data/lib/id3taginator/frames/text/tit1_content_group_description_frame.rb +40 -0
  87. data/lib/id3taginator/frames/text/tit2_title_frame.rb +40 -0
  88. data/lib/id3taginator/frames/text/tit3_subtitle_frame.rb +40 -0
  89. data/lib/id3taginator/frames/text/tkey_initial_key_frame.rb +40 -0
  90. data/lib/id3taginator/frames/text/tlan_language_frame.rb +48 -0
  91. data/lib/id3taginator/frames/text/tlen_length_frame.rb +40 -0
  92. data/lib/id3taginator/frames/text/tmed_media_type_frame.rb +40 -0
  93. data/lib/id3taginator/frames/text/toal_original_album_frame.rb +40 -0
  94. data/lib/id3taginator/frames/text/tofn_original_filename_frame.rb +40 -0
  95. data/lib/id3taginator/frames/text/toly_original_writers_frame.rb +43 -0
  96. data/lib/id3taginator/frames/text/tope_original_artists_frame.rb +43 -0
  97. data/lib/id3taginator/frames/text/tory_original_release_year_frame.rb +42 -0
  98. data/lib/id3taginator/frames/text/town_file_owner_frame.rb +40 -0
  99. data/lib/id3taginator/frames/text/tpe1_artist_frame.rb +43 -0
  100. data/lib/id3taginator/frames/text/tpe2_album_artist_frame.rb +40 -0
  101. data/lib/id3taginator/frames/text/tpe3_conductor_frame.rb +40 -0
  102. data/lib/id3taginator/frames/text/tpe4_modified_by_frame.rb +40 -0
  103. data/lib/id3taginator/frames/text/tpos_part_of_set_frame.rb +50 -0
  104. data/lib/id3taginator/frames/text/tpub_publisher_frame.rb +40 -0
  105. data/lib/id3taginator/frames/text/trck_track_number_frame.rb +50 -0
  106. data/lib/id3taginator/frames/text/trda_recording_dates_frame.rb +42 -0
  107. data/lib/id3taginator/frames/text/trsn_internet_radio_station_frame.rb +40 -0
  108. data/lib/id3taginator/frames/text/tsiz_size_frame.rb +41 -0
  109. data/lib/id3taginator/frames/text/tsoa_album_sort_order_frame.rb +40 -0
  110. data/lib/id3taginator/frames/text/tsop_performer_sort_order_frame.rb +40 -0
  111. data/lib/id3taginator/frames/text/tsot_title_sort_order_frame.rb +40 -0
  112. data/lib/id3taginator/frames/text/tsrc_isrc_frame.rb +40 -0
  113. data/lib/id3taginator/frames/text/tsse_encoder_frame.rb +40 -0
  114. data/lib/id3taginator/frames/text/txxx_user_text_info_frame.rb +51 -0
  115. data/lib/id3taginator/frames/text/tyer_year_frame.rb +42 -0
  116. data/lib/id3taginator/frames/text_frames.rb +840 -0
  117. data/lib/id3taginator/frames/tos/entities/ownership.rb +26 -0
  118. data/lib/id3taginator/frames/tos/entities/terms_of_use.rb +24 -0
  119. data/lib/id3taginator/frames/tos/owne_ownership_frame.rb +53 -0
  120. data/lib/id3taginator/frames/tos/user_terms_of_use_frame.rb +49 -0
  121. data/lib/id3taginator/frames/tos_frames.rb +54 -0
  122. data/lib/id3taginator/frames/ufid/entities/ufid_info.rb +24 -0
  123. data/lib/id3taginator/frames/ufid/ufid_unique_file_identifier_frame.rb +47 -0
  124. data/lib/id3taginator/frames/ufid_frames.rb +40 -0
  125. data/lib/id3taginator/frames/url/entities/user_info.rb +24 -0
  126. data/lib/id3taginator/frames/url/wcom_commercial_url_frame.rb +40 -0
  127. data/lib/id3taginator/frames/url/wcop_copyright_url_frame.rb +40 -0
  128. data/lib/id3taginator/frames/url/woaf_official_file_webpage_frame.rb +40 -0
  129. data/lib/id3taginator/frames/url/woar_official_artist_webpage_frame.rb +40 -0
  130. data/lib/id3taginator/frames/url/woas_official_source_webpage_frame.rb +40 -0
  131. data/lib/id3taginator/frames/url/wors_official_radio_station_homepage_frame.rb +40 -0
  132. data/lib/id3taginator/frames/url/wpay_payment_url_frame.rb +40 -0
  133. data/lib/id3taginator/frames/url/wpub_official_publisher_webpage_frame.rb +40 -0
  134. data/lib/id3taginator/frames/url/wxxx_user_url_link_frame.rb +50 -0
  135. data/lib/id3taginator/frames/url_frames.rb +195 -0
  136. data/lib/id3taginator/genres.rb +168 -0
  137. data/lib/id3taginator/header/id3v23_extended_header.rb +37 -0
  138. data/lib/id3taginator/header/id3v24_extended_header.rb +100 -0
  139. data/lib/id3taginator/header/id3v2_flags.rb +64 -0
  140. data/lib/id3taginator/id3v1_tag.rb +156 -0
  141. data/lib/id3taginator/id3v22_tag.rb +30 -0
  142. data/lib/id3taginator/id3v23_tag.rb +63 -0
  143. data/lib/id3taginator/id3v24_tag.rb +75 -0
  144. data/lib/id3taginator/id3v2_tag.rb +241 -0
  145. data/lib/id3taginator/options/options.rb +33 -0
  146. data/lib/id3taginator/util/compress_util.rb +25 -0
  147. data/lib/id3taginator/util/math_util.rb +68 -0
  148. data/lib/id3taginator/util/sync_util.rb +65 -0
  149. data/lib/id3taginator.rb +449 -0
  150. metadata +198 -0
@@ -0,0 +1,156 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Id3Taginator
4
+ class Id3v1Tag
5
+ include Extensions::Encodable
6
+
7
+ IDENTIFIER = 'TAG'
8
+ TAG_SIZE = 128
9
+
10
+ attr_reader :title, :artist, :album, :year, :comment, :track, :genre
11
+
12
+ # checks if the given stream contains a id3v1 tag
13
+ #
14
+ # @param file [StringIO, IO, File] the file stream to check
15
+ #
16
+ # @return [Boolean] true if contains an id3v1 tag, else false
17
+ def self.id3v1_tag?(file)
18
+ tag = file.read(3)
19
+ file.seek(-3, IO::SEEK_CUR)
20
+
21
+ tag == IDENTIFIER
22
+ end
23
+
24
+ # builds an id3tag from the given file stream
25
+ #
26
+ # @param file [StringIO, IO, File] the file stream
27
+ #
28
+ # @return [Id3v1Tag] the id3v1tag object
29
+ def self.build_from_file(file)
30
+ tag = file.read(3)
31
+ unless tag == IDENTIFIER
32
+ raise Errors::Id3TagError, "#{tag} is no valid Id3v1 identifier. The Tag seems to be corrupted."
33
+ end
34
+
35
+ title = file.read(30)&.strip
36
+ artist = file.read(30)&.strip
37
+ album = file.read(30)&.strip
38
+ year = file.read(4)
39
+ comment = file.read(28)
40
+ track_flag = file.readbyte
41
+ track = nil
42
+ if track_flag.zero?
43
+ track = file.readbyte
44
+ else
45
+ comment += track_flag.chr + file.readbyte.chr
46
+ end
47
+ comment = comment&.strip
48
+ genre = Genres.genre(file.readbyte)
49
+
50
+ new(title, artist, album, year, comment, track, genre)
51
+ end
52
+
53
+ # Constructor
54
+ #
55
+ # @param title [String, nil] the title
56
+ # @param artist [String, nil] the artist
57
+ # @param album [String, nil] the album title
58
+ # @param year [Integer, String, nil] the year
59
+ # @param comment [String, nil] the comment
60
+ # @param track [Integer, nil] the track number
61
+ # @param genre [Symbol, nil] the genre name as a Symbol, e.g. :ROCK
62
+ def initialize(title = nil, artist = nil, album = nil, year = nil, comment = nil, track = nil, genre = :INVALID)
63
+ @title = title
64
+ @artist = artist
65
+ @album = album
66
+ @year = year
67
+ @comment = comment
68
+ @track = track
69
+ @genre = genre
70
+ end
71
+
72
+ # sets a title, up to 30 characters
73
+ #
74
+ # @param title [String] the title
75
+ def title=(title)
76
+ raise Errors::Id3TagError, 'Title can\'t be longer than 30 characters.' if title.length > 30
77
+
78
+ @title = title
79
+ end
80
+
81
+ # sets an artist, up to 30 characters
82
+ #
83
+ # @param artist [String] the artist
84
+ def artist=(artist)
85
+ raise Errors::Id3TagError, 'Artist can\'t be longer than 30 characters.' if artist.length > 30
86
+
87
+ @artist = artist
88
+ end
89
+
90
+ # sets an album title, up to 30 characters
91
+ #
92
+ # @param album [String] the album title
93
+ def album=(album)
94
+ raise Errors::Id3TagError, 'Album can\'t be longer than 30 characters.' if album.length > 30
95
+
96
+ @album = album
97
+ end
98
+
99
+ # sets a year, exactly 4 characters, e.g. 2021
100
+ #
101
+ # @param year [Integer, String] the year
102
+ def year=(year)
103
+ year = year.to_s
104
+ raise Errors::Id3TagError, 'Year must be 4 characters.' if year.length != 4
105
+
106
+ @year = year
107
+ end
108
+
109
+ # sets a comment, up to 30 characters
110
+ #
111
+ # @param comment [String] the comment
112
+ def comment=(comment)
113
+ raise Errors::Id3TagError, 'Comment can\'t be longer than 30 characters.' if comment.length > 30
114
+
115
+ @comment = comment
116
+ end
117
+
118
+ # sets a genre, this must be a SYM from the Genre list in Genres, e.g. :ROCK
119
+ #
120
+ # @param genre_name [Symbol] the genre name as symbol, e.g. :ROCK
121
+ def genre=(genre_name)
122
+ @genre = genre_name.to_sym
123
+ end
124
+
125
+ # sets a track number
126
+ #
127
+ # @param track [Integer] the track, 0 < track# <= 255
128
+ def track=(track)
129
+ track = track.to_i
130
+ raise Errors::Id3TagError, 'Track must be > 0 and < 255.' if !track.nil? && (track.negative? || track > 255)
131
+
132
+ @track = track
133
+ end
134
+
135
+ # dumps the id3v1 tag to a string representing the bytes
136
+ #
137
+ # @return [String] id3v1 byte dump as a string. tag.bytes returns the bytes of the dump
138
+ def to_bytes
139
+ res = 'TAG'
140
+ res += pad_left(@title.nil? ? '' : @title, 30)
141
+ res += pad_left(@artist.nil? ? '' : @artist, 30)
142
+ res += pad_left(@album.nil? ? '' : @album, 30)
143
+ res += pad_left(@year.nil? ? '' : @year&.to_s, 4)
144
+ if @track.nil?
145
+ res += pad_left(@comment.nil? ? '' : @comment, 30)
146
+ else
147
+ res += pad_left(@comment.nil? ? '' : @comment, 28)
148
+ res += 0.chr
149
+ res += @track.chr
150
+ end
151
+ res += Genres.genre_by_name(@genre).chr
152
+
153
+ res
154
+ end
155
+ end
156
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Id3Taginator
4
+ class Id3v22Tag < Id3v2Tag
5
+
6
+ HEADER_SIZE = 10
7
+
8
+ # builds an empty Id3v2.2 tag with the given options set
9
+ #
10
+ # @param options [Options::Options] the options to use
11
+ #
12
+ # @return [Id3v22Tag] returns an empty Id3v2.2 tag
13
+ def self.build_empty(options)
14
+ new(0, Header::Id3v2Flags.new(0x00), 0, StringIO.new, options)
15
+ end
16
+
17
+ # Constructor
18
+ #
19
+ # @param minor_version [Integer] the minor version, in v2.[major].[minor]
20
+ # @param flags [Header::Id3v2Flags] the 2 Byte header flags wrapped in the entity
21
+ # @param tag_size [Integer] number of bytes the excluding header/footer of the tag
22
+ # @param tag_data_stream [StringIO|IO|File] the file stream
23
+ # @param options [Options::Options] the options to use
24
+ def initialize(minor_version, flags, tag_size, tag_data_stream, options)
25
+ @options = options
26
+ frames = tag_size.positive? ? parse_frames(tag_data_stream, 2) : []
27
+ super(HEADER_SIZE, 2, minor_version, flags, tag_size, frames, nil)
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Id3Taginator
4
+ class Id3v23Tag < Id3v2Tag
5
+
6
+ HEADER_SIZE = 10
7
+
8
+ # builds an empty Id3v232 tag with the given options set
9
+ #
10
+ # @param options [Options::Options] the options to use
11
+ #
12
+ # @return [Id3v23Tag] returns an empty Id3v2.3 tag
13
+ def self.build_empty(options)
14
+ new(0, Header::Id3v2Flags.new(0x00), 0, StringIO.new, options)
15
+ end
16
+
17
+ # Constructor
18
+ #
19
+ # @param minor_version [Integer] the minor version, in v2.[major].[minor]
20
+ # @param flags [Header::Id3v2Flags] the 2 Byte header flags wrapped in the entity
21
+ # @param tag_size [Integer] number of bytes the excluding header/footer of the tag
22
+ # @param tag_data_stream [StringIO, IO, File] the file stream
23
+ # @param options [Options::Options] the options to use
24
+ def initialize(minor_version, flags, tag_size, tag_data_stream, options)
25
+ @options = options
26
+ ext_header = flags.extended_header? && tag_size.positive? ? parse_extended_header(tag_data_stream) : nil
27
+ frames = tag_size.positive? ? parse_frames(tag_data_stream, 3) : []
28
+ super(HEADER_SIZE, 3, minor_version, flags, tag_size, frames, ext_header)
29
+ end
30
+
31
+ # parses the extended header if present
32
+ #
33
+ # @param file [StringIO, IO, File] the file stream
34
+ #
35
+ # @return [Header::Id3v23ExtendedHeader] the extended header
36
+ def parse_extended_header(file)
37
+ size = Util::MathUtil.to_number(file.read(4)&.bytes)
38
+ flags = file.read(2)
39
+
40
+ raise Errors::Id3TagError, 'Could not find extended header flag bytes. ID3v2 Tag is corrupt.' if flags.nil?
41
+
42
+ padding = Util::MathUtil.to_number(file.read(4)&.bytes)
43
+
44
+ ext_header = Header::Id3v23ExtendedHeader.new(size, flags, padding)
45
+ ext_header.crc_data = file.read(4) if ext_header.crc?
46
+ ext_header
47
+ end
48
+
49
+ # determines if an extended header is present
50
+ #
51
+ # @return [Boolean] true if header is present, else false
52
+ def extended_header?
53
+ @flags.extended_header?
54
+ end
55
+
56
+ # determines if experimental tags are present
57
+ #
58
+ # @return [Boolean] true if experimental tags present, else false
59
+ def experimental?
60
+ @flags.experimental?
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,75 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Id3Taginator
4
+ class Id3v24Tag < Id3v2Tag
5
+
6
+ HEADER_SIZE = 10
7
+
8
+ # builds an empty Id3v2.4 tag with the given options set
9
+ #
10
+ # @param options [Options::Options] the options to use
11
+ #
12
+ # @return [Id3v24Tag] returns an empty Id3v2.4 tag
13
+ def self.build_empty(options)
14
+ new(0, Header::Id3v2Flags.new(0x00), 0, StringIO.new, options)
15
+ end
16
+
17
+ # Constructor
18
+ #
19
+ # @param minor_version [Integer] the minor version, in v2.[major].[minor]
20
+ # @param flags [Header::Id3v2Flags] the 2 Byte header flags wrapped in the entity
21
+ # @param tag_size [Integer] number of bytes the excluding header/footer of the tag
22
+ # @param tag_data_stream [StringIO, IO, File] the file stream
23
+ # @param options [Options::Options] the options to use
24
+ def initialize(minor_version, flags, tag_size, tag_data_stream, options)
25
+ @options = options
26
+ ext_header = flags.extended_header? && tag_size.positive? ? parse_extended_header(tag_data_stream) : nil
27
+
28
+ frames = tag_size.positive? ? parse_frames(tag_data_stream, 4) : []
29
+ super(HEADER_SIZE, 4, minor_version, flags, tag_size, frames, ext_header)
30
+ end
31
+
32
+ # parses the extended header if present
33
+ #
34
+ # @param file [StringIO, IO, File] the file stream
35
+ #
36
+ # @return [Header::Id3v24ExtendedHeader] the extended header
37
+ def parse_extended_header(file)
38
+ size = Util::MathUtil.to_32_synchsafe_integer(file.read(4)&.bytes)
39
+ number_of_flags = file.readbyte
40
+ flags = file.read(number_of_flags)
41
+
42
+ raise Errors::Id3TagError, 'Could not find extended header flag bytes. ID3v2 Tag is corrupt.' if flags.nil?
43
+
44
+ Header::Id3v24ExtendedHeader.new(size, flags)
45
+ end
46
+
47
+ # determines if an extended header is present
48
+ #
49
+ # @return [Boolean] true if header is present, else false
50
+ def extended_header?
51
+ @flags.extended_header?
52
+ end
53
+
54
+ # determines if experimental tags are present
55
+ #
56
+ # @return [Boolean] true if experimental tags present, else false
57
+ def experimental?
58
+ @flags.experimental?
59
+ end
60
+
61
+ # determines if a footer is present
62
+ #
63
+ # @return [Boolean] true if a footer is present, else false
64
+ def footer?
65
+ @flags.footer?
66
+ end
67
+
68
+ # enables or disables a footer
69
+ #
70
+ # @param enabled [Boolean] true, if the footer should be set to the tag, else false
71
+ def footer=(enabled = true)
72
+ @flags.footer = enabled
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,241 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Id3Taginator
4
+ class Id3v2Tag
5
+ include Frames::UfidFrames
6
+ include Frames::TextFrames
7
+ include Frames::UrlFrames
8
+ include Frames::IplFrames
9
+ include Frames::McdiFrames
10
+ include Frames::LyricsFrames
11
+ include Frames::CommentFrames
12
+ include Frames::PictureFrames
13
+ include Frames::GeoFrames
14
+ include Frames::CountFrames
15
+ include Frames::BufferFrames
16
+ include Frames::ToSFrames
17
+ include Frames::EncryptionFrames
18
+ include Frames::GroupingFrames
19
+ include Frames::PrivateFrames
20
+ include Frames::CustomFrames
21
+
22
+ IDENTIFIER = 'ID3'
23
+
24
+ attr_writer :options
25
+
26
+ # checks if the given stream contains a id3v2 tag
27
+ #
28
+ # @param file [StringIO, IO, File] the file stream to check
29
+ #
30
+ # @return [Boolean] true if contains an id3v2 tag, else false
31
+ def self.id3v2_tag?(file)
32
+ tag = file.read(3)
33
+ file.seek(0)
34
+
35
+ tag == IDENTIFIER
36
+ end
37
+
38
+ # builds an id3tag from the given file stream and the passed options
39
+ #
40
+ # @param file [StringIO, IO, File] the file stream
41
+ # @param options [Options::Options] options that should be used
42
+ #
43
+ # @return [Id3v2Tag] the id3v2tag object,the specific object is determined by the tag version
44
+ def self.build_from_file(file, options)
45
+ file.seek(0)
46
+ tag = file.read(3)
47
+ raise Errors::Id3TagError, "#{tag} is no valid ID3v2 tag. Tag seems corrupted." unless tag == IDENTIFIER
48
+
49
+ major_version = file.readbyte
50
+ minor_version = file.readbyte
51
+ flags = Header::Id3v2Flags.new(file.readbyte)
52
+ tag_size = Util::MathUtil.to_32_synchsafe_integer(file.read(4)&.bytes)
53
+
54
+ tag_data = file.read(tag_size)
55
+ raise Errors::Id3TagError, 'There is no Tag data to read. ID3v2 tag seems to be invalid.' if tag_data.nil?
56
+
57
+ tag_data = Util::SyncUtil.undo_synchronization(StringIO.new(tag_data)) if flags.unsynchronized?
58
+
59
+ id3v2_tag = id3v2_tag_for_version(major_version)
60
+
61
+ id3v2_tag.new(minor_version, flags, tag_size, StringIO.new(tag_data), options)
62
+ end
63
+
64
+ # builds an empty id3tag for the given version
65
+ #
66
+ # @param version [Integer] the id3tag major version 2,3 or 4
67
+ # @param options [Options::Options] the options to use
68
+ #
69
+ # @return [Id3v22Tag, Id3v23Tag, Id3v24Tag] the Id3v2 tag object for the requested version
70
+ def self.build_for_version(version, options)
71
+ case version
72
+ when 2
73
+ Id3v22Tag.build_empty(options)
74
+ when 3
75
+ Id3v23Tag.build_empty(options)
76
+ when 4
77
+ Id3v24Tag.build_empty(options)
78
+ else
79
+ raise Errors::Id3TagError, "Id3v2.#{version} is not supported."
80
+ end
81
+ end
82
+
83
+ # Constructor
84
+ #
85
+ # @param header_size [Integer] number of bytes for the Header, usually 6 for v2 and 10 for v3 and v4
86
+ # @param major_version [Integer] the major version, in v2.[major].[minor]
87
+ # @param minor_version [Integer] the minor version, in v2.[major].[minor]
88
+ # @param flags [Header::Id3v2Flags] the 2 Byte header flags wrapped in the entity
89
+ # @param tag_size [Integer] number of bytes the excluding header/footer of the tag
90
+ # @param frames [Array<Frames::Id3TagFrame>] array of frames of the id3tag
91
+ # @param extended_header [Header::Id3v23ExtendedHeader, Header::Id3v24ExtendedHeader, nil] the extended header of
92
+ # present or nil otherwise
93
+ def initialize(header_size, major_version, minor_version, flags, tag_size, frames, extended_header)
94
+ @header_size = header_size
95
+ @major_version = major_version
96
+ @minor_version = minor_version
97
+ @flags = flags
98
+ @tag_size = tag_size
99
+ @extended_header = extended_header
100
+ @frames = frames
101
+ end
102
+
103
+ # gets the version of this id3tag entity
104
+ #
105
+ # @return [String] returns the version in the form 2.x.y
106
+ def version
107
+ "2.#{@major_version}.#{@minor_version}"
108
+ end
109
+
110
+ # returns the number of bytes of the total tag, including header and footer
111
+ #
112
+ # @return [Integer] the total tag size in bytes
113
+ def total_tag_size
114
+ @header_size + @tag_size + (@flags&.footer? ? 10 : 0)
115
+ end
116
+
117
+ # selects all frames with the given frame id
118
+ #
119
+ # @return [Array<Frames::Id3v2Frame>] an array of Id3v2Frames such as CustomFrame, AlbumFrame etc.
120
+ def frames(frame_id)
121
+ @frames.select { |f| f.frame_id == frame_id }
122
+ end
123
+
124
+ # adds a new frame. There will be no validity checks, even invalid frames can be added, essentially rendering
125
+ # the Id3 tag broken
126
+ #
127
+ # @param frame [Frames::Id3v2Frame] the frame to add
128
+ def add_frame(frame)
129
+ raise Errors::Id3TagError, 'The given frame is no Id3v2Frame.' unless frame.is_a?(Frames::Id3v2Frame)
130
+
131
+ @frames << frame
132
+ end
133
+
134
+ # returns the number of frames for this tag
135
+ #
136
+ # @return [Integer] number of frames
137
+ def number_of_frames
138
+ @frames.length
139
+ end
140
+
141
+ # determined if the tag is unsynchronized
142
+ #
143
+ # @return [Boolean] true if unsynchronized, else false
144
+ def unsynchronized?
145
+ @flags.unsynchronized?
146
+ end
147
+
148
+ # sets the tag synchronized or unsynchronized. Should be false, only required for very old player
149
+ #
150
+ # @param enabled [Boolean] sets unsynchronized enabled or disabled
151
+ def unsynchronized=(enabled = true)
152
+ @flags.unsynchronized = enabled
153
+ end
154
+
155
+ # adds the size tag if not present. If v2.4, the option ignore_v24_frame_error must be true
156
+ #
157
+ # @param audio_size [Integer] the audio size bytes
158
+ def add_size_tag_if_not_present(audio_size)
159
+ return nil unless @options.add_size_frame
160
+
161
+ return nil if @major_version == 4 && !@options.ignore_v24_frame_error
162
+
163
+ size_frame = size
164
+ return nil unless size_frame.nil?
165
+
166
+ self.size = audio_size
167
+ end
168
+
169
+ # dumps the tag to a byte string. This dump already takes unsynchronization, padding and all other options
170
+ # into effect
171
+ #
172
+ # @return [String] tag dump as a String. tag.bytes represents the byte array
173
+ def to_bytes
174
+ # add up frame size and unsyc if necessary
175
+ frames_payload = ''
176
+ @frames.each do |frame|
177
+ frames_payload += frame.to_bytes
178
+ end
179
+
180
+ frames_payload += "\x00" * @options.padding_bytes if @options.padding_bytes.positive?
181
+ frames_payload = Util::SyncUtil.add_synchronization(frames_payload) if unsynchronized?
182
+ frame_size = frames_payload.size
183
+
184
+ res = 'ID3'
185
+ res += @major_version.chr
186
+ res += @minor_version.chr
187
+ res += @flags.to_bytes
188
+ res += Util::MathUtil.from_32_synchsafe_integer(frame_size)
189
+ res += frames_payload
190
+
191
+ if @flags&.footer?
192
+ res = '3DI'
193
+ res += @major_version.chr
194
+ res += @minor_version.chr
195
+ res += @flags.to_bytes
196
+ res += Util::MathUtil.from_32_synchsafe_integer(frame_size)
197
+ end
198
+
199
+ res
200
+ end
201
+
202
+ # creates an id3v2 tag for the specific version
203
+ #
204
+ # @param major_version [Integer] the major version, 2,3 or 4
205
+ #
206
+ # @return [Class<Id3v22Tag>, Class<Id3v23Tag>, Class<Id3v24Tag>] the correct id3v2 tag object
207
+ def self.id3v2_tag_for_version(major_version)
208
+ case major_version
209
+ when 2
210
+ Id3v22Tag
211
+ when 3
212
+ Id3v23Tag
213
+ when 4
214
+ Id3v24Tag
215
+ else
216
+ raise Errors::Id3TagError, "Unsupported version: 2.#{major_version}"
217
+ end
218
+ end
219
+
220
+ # parses the frames for the given id3v2 tag version
221
+ #
222
+ # @param file [StringIO, IO, File] the file or StringIO stream the bytes can be fetched from
223
+ # @param version [Integer]
224
+ #
225
+ # @return [Array<Frames::Id3v2Frame>] list of all parsed frames
226
+ def parse_frames(file, version = 3)
227
+ generator = Frames::Id3v2FrameFactory.new(file, version, @options)
228
+
229
+ result = []
230
+ frame = generator.next_frame
231
+ until frame.nil?
232
+ result << frame
233
+ frame = generator.next_frame
234
+ end
235
+
236
+ result
237
+ end
238
+
239
+ private_class_method :id3v2_tag_for_version
240
+ end
241
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Id3Taginator
4
+ module Options
5
+ class Options
6
+
7
+ attr_accessor :default_encode_dest, :default_decode_dest, :padding_bytes, :ignore_v23_frame_error,
8
+ :ignore_v24_frame_error, :add_size_frame
9
+
10
+ # options that are passed through and applied during read and write
11
+ #
12
+ # @param default_encode_dest [Encoding] destination encoding is the default encoding that is used for the id3tag
13
+ # @param default_decode_dest [Encoding] destination decoding is the default encoding that is used when the id3tag
14
+ # encoding is decoded
15
+ # @param padding_bytes [Integer] the default padding to add at the end of the tag
16
+ # @param ignore_v23_frame_error [Boolean] if true, does not throw an error if a frame is added that is not
17
+ # supported for 2.3 e.g. Sort Order Frames
18
+ # @param ignore_v24_frame_error [Boolean] if true, does not throw an error if a frame is added that is not
19
+ # supported for 2.4 e.g. Size Frame
20
+ # @param add_size_frame [Boolean] if true, the size frame TSIZ will be added automatically to v2.3. Will be added
21
+ # to v2.4 if ignore_v24_frame_error is true
22
+ def initialize(default_encode_dest = Encoding::UTF_16, default_decode_dest = Encoding::UTF_8, padding_bytes = 25,
23
+ ignore_v23_frame_error = true, ignore_v24_frame_error = true, add_size_frame = false)
24
+ @default_encode_dest = default_encode_dest
25
+ @default_decode_dest = default_decode_dest
26
+ @padding_bytes = padding_bytes
27
+ @ignore_v23_frame_error = ignore_v23_frame_error
28
+ @ignore_v24_frame_error = ignore_v24_frame_error
29
+ @add_size_frame = add_size_frame
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Id3Taginator
4
+ module Util
5
+ module CompressUtil
6
+ # compresses the passed data via Zlib
7
+ #
8
+ # @param data [String] the data to compress
9
+ #
10
+ # @return [String] compressed data
11
+ def self.decompress_data(data)
12
+ Zlib::Inflate.inflate(data)
13
+ end
14
+
15
+ # decompresses the passed data via Zlib
16
+ #
17
+ # @param data [String] the data to decompress, this must be compressed by Zlib before
18
+ #
19
+ # @return [String] decompressed data
20
+ def self.compress_data(data)
21
+ Zlib::Deflate.deflate(data)
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Id3Taginator
4
+ module Util
5
+ module MathUtil
6
+ # converts a 4 byte synchsafe array to an integer
7
+ #
8
+ # @param bytes [Array<Integer>, nil] 4 byte array to convert
9
+ #
10
+ # @return [Integer] the 32 bit synchsafe integer
11
+ def self.to_32_synchsafe_integer(bytes)
12
+ raise ArgumentError, 'Input can\'t be nil.' if bytes.nil?
13
+
14
+ size = 0
15
+ bytes&.each_with_index do |byte, index|
16
+ size += 128**(4 - 1 - index) * (byte & 0x7f)
17
+ end
18
+
19
+ size
20
+ end
21
+
22
+ # converts an integer to a 4 byte synchsafe array
23
+ #
24
+ # @param num [Integer] given integer to convert
25
+ #
26
+ # @return [String] 4 byte String representing the 4 byte synchsafe integer (str.bytes)
27
+ def self.from_32_synchsafe_integer(num)
28
+ bytes = Array.new(4, 0)
29
+ bytes.each_with_index do |_, index|
30
+ # noinspection RubyMismatchedParameterType
31
+ bytes[(0 - index).abs], num = num.divmod(128**(4 - 1 - index))
32
+ end
33
+
34
+ bytes.map(&:chr).join
35
+ end
36
+
37
+ # converts a given byte array to the integer representation
38
+ #
39
+ # @param bytes [Array<Integer>, nil] byte array to convert
40
+ #
41
+ # @return [Integer] the integer
42
+ # noinspection RubyNilAnalysis
43
+ def self.to_number(bytes)
44
+ raise ArgumentError, 'Input can\'t be nil.' if bytes.nil?
45
+
46
+ size = 0
47
+ num_bytes = bytes.length
48
+ bytes.each_with_index do |byte, index|
49
+ size += 256**(num_bytes - 1 - index) * (byte & 0xff)
50
+ end
51
+
52
+ size
53
+ end
54
+
55
+ # converts the given number to a byte array, optionally it can be padded using the given char
56
+ # e.g. given 5 and padding 6 will be 00 00 05 and converted to a byte array string
57
+ #
58
+ # @param integer [Integer] the integer to convert
59
+ # @param padding_to [Integer] pad with char if the given number in hex form has less chars than padding_to
60
+ # @param char [String] the character to pad
61
+ #
62
+ # @return [String] the returning byte array in String representation
63
+ def self.from_number(integer, padding_to = 0, char = '0')
64
+ integer.to_s(16).rjust(padding_to, char).scan(/../).map { |x| x.hex.chr }.join
65
+ end
66
+ end
67
+ end
68
+ end