id3taginator 0.8

Sign up to get free protection for your applications and to get access to all the features.
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