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.
- checksums.yaml +7 -0
- data/.gitignore +13 -0
- data/.idea/.gitignore +8 -0
- data/.idea/ID3Taginator.iml +73 -0
- data/.idea/misc.xml +4 -0
- data/.idea/modules.xml +8 -0
- data/.idea/vcs.xml +6 -0
- data/.rspec +3 -0
- data/.rubocop.yml +26 -0
- data/CHANGELOG.md +5 -0
- data/CODE_OF_CONDUCT.md +84 -0
- data/Gemfile +12 -0
- data/LICENSE.txt +21 -0
- data/README.md +408 -0
- data/Rakefile +12 -0
- data/bin/console +15 -0
- data/bin/setup +8 -0
- data/id3taginator.gemspec +37 -0
- data/lib/id3taginator/audio_file.rb +136 -0
- data/lib/id3taginator/errors/id3_tag_error.rb +12 -0
- data/lib/id3taginator/extensions/argument_check.rb +88 -0
- data/lib/id3taginator/extensions/comparable.rb +28 -0
- data/lib/id3taginator/extensions/encodable.rb +215 -0
- data/lib/id3taginator/extensions/optionable.rb +73 -0
- data/lib/id3taginator/frames/buffer/entities/buffer.rb +26 -0
- data/lib/id3taginator/frames/buffer/rbuf_recommended_buffer_size_frame.rb +59 -0
- data/lib/id3taginator/frames/buffer_frames.rb +32 -0
- data/lib/id3taginator/frames/comment/comm_comment_frame.rb +56 -0
- data/lib/id3taginator/frames/comment/entities/comment.rb +26 -0
- data/lib/id3taginator/frames/comment_frames.rb +42 -0
- data/lib/id3taginator/frames/count/entities/popularimeter.rb +26 -0
- data/lib/id3taginator/frames/count/pcnt_play_counter_frame.rb +40 -0
- data/lib/id3taginator/frames/count/popm_popularimeter_frame.rb +52 -0
- data/lib/id3taginator/frames/count_frames.rb +51 -0
- data/lib/id3taginator/frames/custom_frame.rb +39 -0
- data/lib/id3taginator/frames/custom_frames.rb +54 -0
- data/lib/id3taginator/frames/encryption/aenc_audio_encryption.rb +54 -0
- data/lib/id3taginator/frames/encryption/encr_encryption_method_frame.rb +49 -0
- data/lib/id3taginator/frames/encryption/entities/audio_encryption.rb +28 -0
- data/lib/id3taginator/frames/encryption/entities/encryption_method.rb +26 -0
- data/lib/id3taginator/frames/encryption_frames.rb +77 -0
- data/lib/id3taginator/frames/frameable.rb +75 -0
- data/lib/id3taginator/frames/geo/entities/encapsulated_object.rb +28 -0
- data/lib/id3taginator/frames/geo/geob_general_encapsulated_object_frame.rb +59 -0
- data/lib/id3taginator/frames/geo_frames.rb +41 -0
- data/lib/id3taginator/frames/grouping/entities/group_identification.rb +26 -0
- data/lib/id3taginator/frames/grouping/grid_group_identification_frame.rb +49 -0
- data/lib/id3taginator/frames/grouping/grp1_grouping_frame.rb +40 -0
- data/lib/id3taginator/frames/grouping_frames.rb +61 -0
- data/lib/id3taginator/frames/has_id.rb +51 -0
- data/lib/id3taginator/frames/id3v23_frame_flags.rb +64 -0
- data/lib/id3taginator/frames/id3v24_frame_flags.rb +80 -0
- data/lib/id3taginator/frames/id3v2_frame.rb +249 -0
- data/lib/id3taginator/frames/id3v2_frame_factory.rb +98 -0
- data/lib/id3taginator/frames/ipl/entities/involved_person.rb +24 -0
- data/lib/id3taginator/frames/ipl/ipls_involved_people_frame.rb +48 -0
- data/lib/id3taginator/frames/ipl_frames.rb +31 -0
- data/lib/id3taginator/frames/lyrics/entities/unsync_lyrics.rb +26 -0
- data/lib/id3taginator/frames/lyrics/uslt_unsync_lyrics_frame.rb +56 -0
- data/lib/id3taginator/frames/lyrics_frames.rb +42 -0
- data/lib/id3taginator/frames/mcdi/mcdi_music_cd_identifier_frame.rb +40 -0
- data/lib/id3taginator/frames/mcdi_frames.rb +28 -0
- data/lib/id3taginator/frames/picture/apic_picture_frame.rb +106 -0
- data/lib/id3taginator/frames/picture/entities/picture.rb +32 -0
- data/lib/id3taginator/frames/picture_frames.rb +51 -0
- data/lib/id3taginator/frames/private/entities/private_frame.rb +24 -0
- data/lib/id3taginator/frames/private/priv_private_frame.rb +45 -0
- data/lib/id3taginator/frames/private_frames.rb +40 -0
- data/lib/id3taginator/frames/text/entities/copyright.rb +24 -0
- data/lib/id3taginator/frames/text/entities/date.rb +24 -0
- data/lib/id3taginator/frames/text/entities/part_of_set.rb +24 -0
- data/lib/id3taginator/frames/text/entities/time.rb +24 -0
- data/lib/id3taginator/frames/text/entities/track_number.rb +24 -0
- data/lib/id3taginator/frames/text/entities/user_info.rb +24 -0
- data/lib/id3taginator/frames/text/talb_album_frame.rb +40 -0
- data/lib/id3taginator/frames/text/tbpm_bpm_frame.rb +40 -0
- data/lib/id3taginator/frames/text/tcom_composer_frame.rb +42 -0
- data/lib/id3taginator/frames/text/tcon_genre_frame.rb +104 -0
- data/lib/id3taginator/frames/text/tcop_copyright_frame.rb +55 -0
- data/lib/id3taginator/frames/text/tdat_date_frame.rb +60 -0
- data/lib/id3taginator/frames/text/tdly_playlist_delay_frame.rb +40 -0
- data/lib/id3taginator/frames/text/tenc_encoded_by_frame.rb +40 -0
- data/lib/id3taginator/frames/text/text_writers_frame.rb +43 -0
- data/lib/id3taginator/frames/text/tflt_file_type_frame.rb +71 -0
- data/lib/id3taginator/frames/text/time_time_frame.rb +60 -0
- data/lib/id3taginator/frames/text/tit1_content_group_description_frame.rb +40 -0
- data/lib/id3taginator/frames/text/tit2_title_frame.rb +40 -0
- data/lib/id3taginator/frames/text/tit3_subtitle_frame.rb +40 -0
- data/lib/id3taginator/frames/text/tkey_initial_key_frame.rb +40 -0
- data/lib/id3taginator/frames/text/tlan_language_frame.rb +48 -0
- data/lib/id3taginator/frames/text/tlen_length_frame.rb +40 -0
- data/lib/id3taginator/frames/text/tmed_media_type_frame.rb +40 -0
- data/lib/id3taginator/frames/text/toal_original_album_frame.rb +40 -0
- data/lib/id3taginator/frames/text/tofn_original_filename_frame.rb +40 -0
- data/lib/id3taginator/frames/text/toly_original_writers_frame.rb +43 -0
- data/lib/id3taginator/frames/text/tope_original_artists_frame.rb +43 -0
- data/lib/id3taginator/frames/text/tory_original_release_year_frame.rb +42 -0
- data/lib/id3taginator/frames/text/town_file_owner_frame.rb +40 -0
- data/lib/id3taginator/frames/text/tpe1_artist_frame.rb +43 -0
- data/lib/id3taginator/frames/text/tpe2_album_artist_frame.rb +40 -0
- data/lib/id3taginator/frames/text/tpe3_conductor_frame.rb +40 -0
- data/lib/id3taginator/frames/text/tpe4_modified_by_frame.rb +40 -0
- data/lib/id3taginator/frames/text/tpos_part_of_set_frame.rb +50 -0
- data/lib/id3taginator/frames/text/tpub_publisher_frame.rb +40 -0
- data/lib/id3taginator/frames/text/trck_track_number_frame.rb +50 -0
- data/lib/id3taginator/frames/text/trda_recording_dates_frame.rb +42 -0
- data/lib/id3taginator/frames/text/trsn_internet_radio_station_frame.rb +40 -0
- data/lib/id3taginator/frames/text/tsiz_size_frame.rb +41 -0
- data/lib/id3taginator/frames/text/tsoa_album_sort_order_frame.rb +40 -0
- data/lib/id3taginator/frames/text/tsop_performer_sort_order_frame.rb +40 -0
- data/lib/id3taginator/frames/text/tsot_title_sort_order_frame.rb +40 -0
- data/lib/id3taginator/frames/text/tsrc_isrc_frame.rb +40 -0
- data/lib/id3taginator/frames/text/tsse_encoder_frame.rb +40 -0
- data/lib/id3taginator/frames/text/txxx_user_text_info_frame.rb +51 -0
- data/lib/id3taginator/frames/text/tyer_year_frame.rb +42 -0
- data/lib/id3taginator/frames/text_frames.rb +840 -0
- data/lib/id3taginator/frames/tos/entities/ownership.rb +26 -0
- data/lib/id3taginator/frames/tos/entities/terms_of_use.rb +24 -0
- data/lib/id3taginator/frames/tos/owne_ownership_frame.rb +53 -0
- data/lib/id3taginator/frames/tos/user_terms_of_use_frame.rb +49 -0
- data/lib/id3taginator/frames/tos_frames.rb +54 -0
- data/lib/id3taginator/frames/ufid/entities/ufid_info.rb +24 -0
- data/lib/id3taginator/frames/ufid/ufid_unique_file_identifier_frame.rb +47 -0
- data/lib/id3taginator/frames/ufid_frames.rb +40 -0
- data/lib/id3taginator/frames/url/entities/user_info.rb +24 -0
- data/lib/id3taginator/frames/url/wcom_commercial_url_frame.rb +40 -0
- data/lib/id3taginator/frames/url/wcop_copyright_url_frame.rb +40 -0
- data/lib/id3taginator/frames/url/woaf_official_file_webpage_frame.rb +40 -0
- data/lib/id3taginator/frames/url/woar_official_artist_webpage_frame.rb +40 -0
- data/lib/id3taginator/frames/url/woas_official_source_webpage_frame.rb +40 -0
- data/lib/id3taginator/frames/url/wors_official_radio_station_homepage_frame.rb +40 -0
- data/lib/id3taginator/frames/url/wpay_payment_url_frame.rb +40 -0
- data/lib/id3taginator/frames/url/wpub_official_publisher_webpage_frame.rb +40 -0
- data/lib/id3taginator/frames/url/wxxx_user_url_link_frame.rb +50 -0
- data/lib/id3taginator/frames/url_frames.rb +195 -0
- data/lib/id3taginator/genres.rb +168 -0
- data/lib/id3taginator/header/id3v23_extended_header.rb +37 -0
- data/lib/id3taginator/header/id3v24_extended_header.rb +100 -0
- data/lib/id3taginator/header/id3v2_flags.rb +64 -0
- data/lib/id3taginator/id3v1_tag.rb +156 -0
- data/lib/id3taginator/id3v22_tag.rb +30 -0
- data/lib/id3taginator/id3v23_tag.rb +63 -0
- data/lib/id3taginator/id3v24_tag.rb +75 -0
- data/lib/id3taginator/id3v2_tag.rb +241 -0
- data/lib/id3taginator/options/options.rb +33 -0
- data/lib/id3taginator/util/compress_util.rb +25 -0
- data/lib/id3taginator/util/math_util.rb +68 -0
- data/lib/id3taginator/util/sync_util.rb +65 -0
- data/lib/id3taginator.rb +449 -0
- 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
|