├── test └── id3_test.dart ├── pubspec.yaml ├── .metadata ├── example └── main.dart ├── LICENSE ├── CHANGELOG.md ├── README.md ├── .gitignore ├── lib ├── src │ └── const.dart └── id3.dart └── pubspec.lock /test/id3_test.dart: -------------------------------------------------------------------------------- 1 | void main() { 2 | 3 | } -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: id3 2 | description: A cross platform dart package to extract meta data from mp3 files. 3 | version: 1.0.2 4 | homepage: https://github.com/sanket143/id3 5 | 6 | environment: 7 | sdk: '>=2.12.0 <3.0.0' 8 | 9 | dev_dependencies: 10 | test: ^1.17.10 11 | -------------------------------------------------------------------------------- /.metadata: -------------------------------------------------------------------------------- 1 | # This file tracks properties of this Flutter project. 2 | # Used by Flutter tool to assess capabilities and perform upgrades etc. 3 | # 4 | # This file should be version controlled and should not be manually edited. 5 | 6 | version: 7 | revision: 102ab1e6d989de57019e0b1ee003ccb311ca3095 8 | channel: master 9 | 10 | project_type: package 11 | -------------------------------------------------------------------------------- /example/main.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | import 'package:id3/id3.dart'; 3 | 4 | void main(){ 5 | List mp3Bytes = File("./file.mp3").readAsBytesSync(); 6 | MP3Instance mp3instance = new MP3Instance(mp3Bytes); 7 | if(mp3instance.parseTagsSync()){ 8 | print(mp3instance.getMetaTags()); 9 | } 10 | } 11 | 12 | // { 13 | // "Title": "SongName", 14 | // "Artist": "ArtistName", 15 | // "Album": "AlbumName", 16 | // "APIC": { 17 | // "mime": "image/jpeg", 18 | // "textEncoding": "0", 19 | // "picType": "0", 20 | // "description": "description", 21 | // "base64": "AP/Y/+AAEEpGSUYAAQEBAE..." 22 | // } 23 | // } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Sanket Chaudhari 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## [1.0.2] - 2021-11-08 2 | * [Parse PicType, fix picture on ID3v2.2](https://github.com/sanket143/id3/pull/19) 3 | * Welcome [@moffatman](https://github.com/moffatman) as a collaborator 4 | * [Properly parse ID3v2.2 tags](https://github.com/sanket143/id3/pull/14) 5 | 6 | ## [1.0.1] - 2021-08-28 7 | * [Properly parse ID3v2.2 tags](https://github.com/sanket143/id3/pull/14), with some explicit type notation 8 | 9 | ## [1.0.0] - 2021-07-17 10 | * [API Update](https://github.com/sanket143/id3/pull/10), takes bytes instead of fileName as argument in `MP3Instance` 11 | * [Fix corrupted album art](https://github.com/sanket143/id3/pull/9) [Thanks to [@moffotman](https://github.com/moffatman)] 12 | * [Migration to null safety](https://github.com/sanket143/id3/pull/6) [Thanks to [@4-alok](https://github.com/4-alok)] 13 | 14 | ## [0.1.8] - 2020-01-08 15 | 16 | * Removed redundant "this" and "new" keyword 17 | 18 | ## [0.1.3] - 2019-08-07 19 | 20 | * Renamed parseTagsSync to parseTagsSync 21 | 22 | ## [0.1.1] - 2019-07-24 23 | 24 | * First public release with id3v1 and id3v2.3 support 25 | 26 | ## [0.0.5] - 2019-07-19 27 | 28 | * Added id3v1 support 29 | 30 | ## [0.0.4] - 2019-07-18 31 | 32 | * Initial Release 33 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # id3 2 | A cross platform dart package to extract meta data from mp3 files. 3 | 4 | This package contains functions that could extract meta tags from mp3 5 | files that uses ``ID3 tag version 2.3.0`` and ``ID3 tag version 1`` to store meta data. 6 | 7 | ## Usage 8 | 9 | ```dart 10 | import 'dart:io'; 11 | import 'package:id3/id3.dart'; 12 | 13 | void main(){ 14 | List mp3Bytes = File("./file.mp3").readAsBytesSync(); 15 | MP3Instance mp3instance = new MP3Instance(mp3Bytes); 16 | 17 | /// parseTagsSync() returns 18 | // 'true' if successfully parsed 19 | // 'false' if was unable to recognize tag so can't be parsed 20 | 21 | if(mp3instance.parseTagsSync()){ 22 | print(mp3instance.getMetaTags()); 23 | } 24 | } 25 | 26 | /// mp3instance.getMetaTags() returns Map 27 | // { 28 | // "Title": "SongName", 29 | // "Artist": "ArtistName", 30 | // "Album": "AlbumName", 31 | // "APIC": { 32 | // "mime": "image/jpeg", 33 | // "textEncoding": "0", 34 | // "picType": "0", 35 | // "description": "description", 36 | // "base64": "AP/Y/+AAEEpGSUYAAQEBAE..." 37 | // } 38 | // } 39 | ``` 40 | 41 | # Migrating from v0.1 to v1.0 42 | 43 | - `MP3Instance` used to take filename as string in v0.1, which is updated to take bytes as `List` in v1.0 44 | 45 | ```diff 46 | - MP3Instance mp3instance = new MP3Instance("./file.mp3"); 47 | + List mp3Bytes = File("./file.mp3").readAsBytesSync(); 48 | + MP3Instance mp3instance = new MP3Instance(mp3Bytes); 49 | ``` 50 | 51 | ### [**Looking for a new maintainer**](https://github.com/sanket143/id3/issues/27) 52 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.log 4 | *.pyc 5 | *.swp 6 | .DS_Store 7 | .atom/ 8 | .buildlog/ 9 | .history 10 | .svn/ 11 | 12 | # IntelliJ related 13 | *.iml 14 | *.ipr 15 | *.iws 16 | .idea/ 17 | 18 | # The .vscode folder contains launch configuration and tasks you configure in 19 | # VS Code which you may wish to be included in version control, so this line 20 | # is commented out by default. 21 | .vscode/ 22 | 23 | test/test.dart 24 | 25 | # Flutter/Dart/Pub related 26 | **/doc/api/ 27 | .dart_tool/ 28 | .flutter-plugins 29 | .packages 30 | .pub-cache/ 31 | .pub/ 32 | build/ 33 | 34 | # Android related 35 | **/android/**/gradle-wrapper.jar 36 | **/android/.gradle 37 | **/android/captures/ 38 | **/android/gradlew 39 | **/android/gradlew.bat 40 | **/android/local.properties 41 | **/android/**/GeneratedPluginRegistrant.java 42 | 43 | # iOS/XCode related 44 | **/ios/**/*.mode1v3 45 | **/ios/**/*.mode2v3 46 | **/ios/**/*.moved-aside 47 | **/ios/**/*.pbxuser 48 | **/ios/**/*.perspectivev3 49 | **/ios/**/*sync/ 50 | **/ios/**/.sconsign.dblite 51 | **/ios/**/.tags* 52 | **/ios/**/.vagrant/ 53 | **/ios/**/DerivedData/ 54 | **/ios/**/Icon? 55 | **/ios/**/Pods/ 56 | **/ios/**/.symlinks/ 57 | **/ios/**/profile 58 | **/ios/**/xcuserdata 59 | **/ios/.generated/ 60 | **/ios/Flutter/App.framework 61 | **/ios/Flutter/Flutter.framework 62 | **/ios/Flutter/Generated.xcconfig 63 | **/ios/Flutter/app.flx 64 | **/ios/Flutter/app.zip 65 | **/ios/Flutter/flutter_assets/ 66 | **/ios/ServiceDefinitions.json 67 | **/ios/Runner/GeneratedPluginRegistrant.* 68 | 69 | # Exceptions to above rules. 70 | !**/ios/**/default.mode1v3 71 | !**/ios/**/default.mode2v3 72 | !**/ios/**/default.pbxuser 73 | !**/ios/**/default.perspectivev3 74 | !/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages 75 | -------------------------------------------------------------------------------- /lib/src/const.dart: -------------------------------------------------------------------------------- 1 | const Map FRAMESv2_2 = { 2 | 'CNT': 'PlayCounter', 3 | 'COM': 'Comment', 4 | 'CRA': 'Encryption', 5 | 'EQU': 'Equalization', 6 | 'IPL': 'People', 7 | 'LNK': 'Information', 8 | 'PIC': 'APIC', 9 | 'POP': 'Popularimeter', 10 | 'REV': 'Reverb', 11 | 'SLT': 'Transcript', 12 | 'TAL': 'Album', 13 | 'TBP': 'BPM', 14 | 'TCM': 'Composer', 15 | 'TCO': 'ContentType', 16 | 'TCR': 'Copyright', 17 | 'TDA': 'Date', 18 | 'TEN': 'EncodedBy', 19 | 'TFT': 'FileType', 20 | 'TIM': 'Time', 21 | 'TKE': 'Key', 22 | 'TLA': 'Language', 23 | 'TLE': 'Length', 24 | 'TMT': 'MediaType', 25 | 'TOA': 'OriginalPerformer', 26 | 'TOF': 'OriginalFilename', 27 | 'TOL': 'OriginalLyricist', 28 | 'TOR': 'OriginalReleaseYear', 29 | 'TOT': 'OriginalAlbum', 30 | 'TP1': 'Artist', 31 | 'TP2': 'Accompaniment', 32 | 'TP3': 'Conductor', 33 | 'TP4': 'ModifiedBy', 34 | 'TPB': 'Publisher', 35 | 'TRC': 'ISRC', 36 | 'TRK': 'Track', 37 | 'TSI': 'Size', 38 | 'TSS': 'Settings', 39 | 'TT1': 'Content', 40 | 'TT2': 'Title', 41 | 'TT3': 'SubTitle', 42 | 'TXT': 'Lyricist', 43 | 'TXX': 'AdditionalInfo', 44 | 'TYE': 'Year', 45 | 'UFI': 'ID', 46 | 'ULT': 'UnsyncTranscript', 47 | 'WAF': 'OFileWebpage', 48 | 'WAR': 'OArtistWebpage', 49 | 'WAS': 'OAudioWebpage', 50 | 'WCM': 'CommercialInfo', 51 | 'WCP': 'CopyrightInfo', 52 | 'WPB': 'OPublisherWebpage', 53 | 'WXX': 'CLink' 54 | }; 55 | 56 | const Map FRAMESv2_3 = { 57 | 'AENC': 'Encryption', 58 | 'APIC': 'APIC', 59 | 'COMM': 'Comment', 60 | 'COMR': 'Commercial', 61 | 'ENCR': 'ENCR', 62 | 'EQUA': 'Equalization', 63 | 'ETCO': 'ETCO', 64 | 'GEOB': 'GEOB', 65 | 'GRID': 'GRID', 66 | 'IPLS': 'People', 67 | 'LINK': 'Information', 68 | 'MCDI': 'MCDI', 69 | 'MLLT': 'MLLT', 70 | 'OWNE': 'OWNE', 71 | 'PRIV': 'PRIV', 72 | 'PCNT': 'PlayCounter', 73 | 'POPM': 'Popularimeter', 74 | 'POSS': 'POSS', 75 | 'RBUF': 'RBUF', 76 | 'RVAD': 'RVAD', 77 | 'SYLT': 'Lyrics', 78 | 'SYTC': 'SYTC', 79 | 'TALB': 'Album', 80 | 'TBPM': 'BPM', 81 | 'TCOM': 'Composer', 82 | 'TCON': 'Genre', 83 | 'TCOP': 'Copyright', 84 | 'TDAT': 'Date', 85 | 'TDLY': 'Playlist', 86 | 'TENC': 'EncodedBy', 87 | 'TEXT': 'Lyricist', 88 | 'TFLT': 'FileType', 89 | 'TIME': 'Time', 90 | 'TIT1': 'Content', 91 | 'TIT2': 'Title', 92 | 'TIT3': 'SubTitle', 93 | 'TKEY': 'Key', 94 | 'TLAN': 'Language', 95 | 'TLEN': 'Length', 96 | 'TMED': 'MediaType', 97 | 'TOAL': 'OriginalAlbum', 98 | 'TOFN': 'OriginalFilename', 99 | 'TOLY': 'OriginalLyricist', 100 | 'TOPE': 'OriginalPerformer', 101 | 'TORY': 'OriginalReleaseYear', 102 | 'TOWN': 'Owner', 103 | 'TPE1': 'Artist', 104 | 'TPE2': 'Accompaniment', 105 | 'TPE3': 'Conductor', 106 | 'TPE4': 'ModifiedBy', 107 | 'TPOS': 'TPOS', 108 | 'TPUB': 'Publisher', 109 | 'TRCK': 'Track', 110 | 'TRDA': 'RecordedOn', 111 | 'TRSN': 'RadioStation', 112 | 'TRSO': 'RadioStationOwner', 113 | 'TSIZ': 'Size', 114 | 'TSRC': 'ISRC', 115 | 'TSSE': 'Settings', 116 | 'TYER': 'Year', 117 | 'TXXX': 'AdditionalInfo', 118 | 'UFID': 'ID', 119 | 'USER': 'Terms', 120 | 'USLT': 'UnsyncTranscript', 121 | 'WCOM': 'CommercialInfo', 122 | 'WCOP': 'CopyrightInfo', 123 | 'WOAF': 'OFileWebpage', 124 | 'WOAR': 'OArtistWebpage', 125 | 'WOAS': 'OAudioWebpage', 126 | 'WORS': 'ORadioWebpage', 127 | 'WPAY': 'Payment', 128 | 'WPUB': 'OPublisherWebpage', 129 | 'WXXX': 'CLink' 130 | }; 131 | 132 | const GENREv1 = [ 133 | 'Blues', 134 | 'Classic Rock', 135 | 'Country', 136 | 'Dance', 137 | 'Disco', 138 | 'Funk', 139 | 'Grunge', 140 | 'Hip-Hop', 141 | 'Jazz', 142 | 'Metal', 143 | 'New Age', 144 | 'Oldies', 145 | 'Other', 146 | 'Pop', 147 | 'Rhythm and Blues', 148 | 'Rap', 149 | 'Reggae', 150 | 'Rock', 151 | 'Techno', 152 | 'Industrial', 153 | 'Alternative', 154 | 'Ska', 155 | 'Death Metal', 156 | 'Pranks', 157 | 'Soundtrack', 158 | 'Euro-Techno', 159 | 'Ambient', 160 | 'Trip-Hop', 161 | 'Vocal', 162 | 'Jazz & Funk', 163 | 'Fusion', 164 | 'Trance', 165 | 'Classical', 166 | 'Instrumental', 167 | 'Acid', 168 | 'House', 169 | 'Game', 170 | 'Sound clip', 171 | 'Gospel', 172 | 'Noise', 173 | 'Alternative Rock', 174 | 'Bass', 175 | 'Soul', 176 | 'Punk', 177 | 'Space', 178 | 'Meditative', 179 | 'Instrumental Pop', 180 | 'Instrumental Rock', 181 | 'Ethnic', 182 | 'Gothic', 183 | 'Darkwave', 184 | 'Techno-Industrial', 185 | 'Electronic', 186 | 'Pop-Folk', 187 | 'Eurodance', 188 | 'Dream', 189 | 'Southern Rock', 190 | 'Comedy', 191 | 'Cult', 192 | 'Gangsta', 193 | 'Top 40', 194 | 'Christian Rap', 195 | 'Pop/Funk', 196 | 'Jungle music', 197 | 'Native US', 198 | 'Cabaret', 199 | 'New Wave', 200 | 'Psychedelic', 201 | 'Rave', 202 | 'Showtunes', 203 | 'Trailer', 204 | 'Lo-Fi', 205 | 'Tribal', 206 | 'Acid Punk', 207 | 'Acid Jazz', 208 | 'Polka', 209 | 'Retro', 210 | 'Musical', 211 | 'Rock ’n’ Roll', 212 | 'Hard Rock' 213 | ]; 214 | 215 | const PICTYPE = { 216 | 0x00: 'Other', 217 | 0x01: 'FileIcon', 218 | 0x02: 'OtherFileIcon', 219 | 0x03: 'FrontCover', 220 | 0x04: 'BackCover', 221 | 0x05: 'LeafletPage', 222 | 0x06: 'Media (e.g. lable side of CD)', 223 | 0x07: 'LeadArtist', 224 | 0x08: 'Artist', 225 | 0x09: 'Conductor', 226 | 0x0A: 'Band', 227 | 0x0B: 'Composer', 228 | 0x0C: 'Lyricist', 229 | 0x0D: 'RecordingLocation', 230 | 0x0E: 'DuringRecording', 231 | 0x0F: 'DuringPerformance', 232 | 0x10: 'VideoStill', 233 | // Might be a joke but it's in the ID3 spec 234 | 0x11: 'ABrightColouredFish', 235 | 0x12: 'Illustration', 236 | 0x13: 'ArtistLogo', 237 | 0x14: 'PublisherLogo' 238 | }; 239 | -------------------------------------------------------------------------------- /pubspec.lock: -------------------------------------------------------------------------------- 1 | # Generated by pub 2 | # See https://dart.dev/tools/pub/glossary#lockfile 3 | packages: 4 | _fe_analyzer_shared: 5 | dependency: transitive 6 | description: 7 | name: _fe_analyzer_shared 8 | url: "https://pub.dartlang.org" 9 | source: hosted 10 | version: "23.0.0" 11 | analyzer: 12 | dependency: transitive 13 | description: 14 | name: analyzer 15 | url: "https://pub.dartlang.org" 16 | source: hosted 17 | version: "2.0.0" 18 | args: 19 | dependency: transitive 20 | description: 21 | name: args 22 | url: "https://pub.dartlang.org" 23 | source: hosted 24 | version: "2.1.1" 25 | async: 26 | dependency: transitive 27 | description: 28 | name: async 29 | url: "https://pub.dartlang.org" 30 | source: hosted 31 | version: "2.7.0" 32 | boolean_selector: 33 | dependency: transitive 34 | description: 35 | name: boolean_selector 36 | url: "https://pub.dartlang.org" 37 | source: hosted 38 | version: "2.1.0" 39 | charcode: 40 | dependency: transitive 41 | description: 42 | name: charcode 43 | url: "https://pub.dartlang.org" 44 | source: hosted 45 | version: "1.3.1" 46 | cli_util: 47 | dependency: transitive 48 | description: 49 | name: cli_util 50 | url: "https://pub.dartlang.org" 51 | source: hosted 52 | version: "0.3.3" 53 | collection: 54 | dependency: transitive 55 | description: 56 | name: collection 57 | url: "https://pub.dartlang.org" 58 | source: hosted 59 | version: "1.15.0" 60 | convert: 61 | dependency: transitive 62 | description: 63 | name: convert 64 | url: "https://pub.dartlang.org" 65 | source: hosted 66 | version: "3.0.1" 67 | coverage: 68 | dependency: transitive 69 | description: 70 | name: coverage 71 | url: "https://pub.dartlang.org" 72 | source: hosted 73 | version: "1.0.3" 74 | crypto: 75 | dependency: transitive 76 | description: 77 | name: crypto 78 | url: "https://pub.dartlang.org" 79 | source: hosted 80 | version: "3.0.1" 81 | file: 82 | dependency: transitive 83 | description: 84 | name: file 85 | url: "https://pub.dartlang.org" 86 | source: hosted 87 | version: "6.1.2" 88 | frontend_server_client: 89 | dependency: transitive 90 | description: 91 | name: frontend_server_client 92 | url: "https://pub.dartlang.org" 93 | source: hosted 94 | version: "2.1.0" 95 | glob: 96 | dependency: transitive 97 | description: 98 | name: glob 99 | url: "https://pub.dartlang.org" 100 | source: hosted 101 | version: "2.0.1" 102 | http_multi_server: 103 | dependency: transitive 104 | description: 105 | name: http_multi_server 106 | url: "https://pub.dartlang.org" 107 | source: hosted 108 | version: "3.0.1" 109 | http_parser: 110 | dependency: transitive 111 | description: 112 | name: http_parser 113 | url: "https://pub.dartlang.org" 114 | source: hosted 115 | version: "4.0.0" 116 | io: 117 | dependency: transitive 118 | description: 119 | name: io 120 | url: "https://pub.dartlang.org" 121 | source: hosted 122 | version: "1.0.3" 123 | js: 124 | dependency: transitive 125 | description: 126 | name: js 127 | url: "https://pub.dartlang.org" 128 | source: hosted 129 | version: "0.6.3" 130 | logging: 131 | dependency: transitive 132 | description: 133 | name: logging 134 | url: "https://pub.dartlang.org" 135 | source: hosted 136 | version: "1.0.1" 137 | matcher: 138 | dependency: transitive 139 | description: 140 | name: matcher 141 | url: "https://pub.dartlang.org" 142 | source: hosted 143 | version: "0.12.10" 144 | meta: 145 | dependency: transitive 146 | description: 147 | name: meta 148 | url: "https://pub.dartlang.org" 149 | source: hosted 150 | version: "1.7.0" 151 | mime: 152 | dependency: transitive 153 | description: 154 | name: mime 155 | url: "https://pub.dartlang.org" 156 | source: hosted 157 | version: "1.0.0" 158 | node_preamble: 159 | dependency: transitive 160 | description: 161 | name: node_preamble 162 | url: "https://pub.dartlang.org" 163 | source: hosted 164 | version: "2.0.1" 165 | package_config: 166 | dependency: transitive 167 | description: 168 | name: package_config 169 | url: "https://pub.dartlang.org" 170 | source: hosted 171 | version: "2.0.0" 172 | path: 173 | dependency: transitive 174 | description: 175 | name: path 176 | url: "https://pub.dartlang.org" 177 | source: hosted 178 | version: "1.8.0" 179 | pedantic: 180 | dependency: transitive 181 | description: 182 | name: pedantic 183 | url: "https://pub.dartlang.org" 184 | source: hosted 185 | version: "1.11.1" 186 | pool: 187 | dependency: transitive 188 | description: 189 | name: pool 190 | url: "https://pub.dartlang.org" 191 | source: hosted 192 | version: "1.5.0" 193 | pub_semver: 194 | dependency: transitive 195 | description: 196 | name: pub_semver 197 | url: "https://pub.dartlang.org" 198 | source: hosted 199 | version: "2.0.0" 200 | shelf: 201 | dependency: transitive 202 | description: 203 | name: shelf 204 | url: "https://pub.dartlang.org" 205 | source: hosted 206 | version: "1.2.0" 207 | shelf_packages_handler: 208 | dependency: transitive 209 | description: 210 | name: shelf_packages_handler 211 | url: "https://pub.dartlang.org" 212 | source: hosted 213 | version: "3.0.0" 214 | shelf_static: 215 | dependency: transitive 216 | description: 217 | name: shelf_static 218 | url: "https://pub.dartlang.org" 219 | source: hosted 220 | version: "1.1.0" 221 | shelf_web_socket: 222 | dependency: transitive 223 | description: 224 | name: shelf_web_socket 225 | url: "https://pub.dartlang.org" 226 | source: hosted 227 | version: "1.0.1" 228 | source_map_stack_trace: 229 | dependency: transitive 230 | description: 231 | name: source_map_stack_trace 232 | url: "https://pub.dartlang.org" 233 | source: hosted 234 | version: "2.1.0" 235 | source_maps: 236 | dependency: transitive 237 | description: 238 | name: source_maps 239 | url: "https://pub.dartlang.org" 240 | source: hosted 241 | version: "0.10.10" 242 | source_span: 243 | dependency: transitive 244 | description: 245 | name: source_span 246 | url: "https://pub.dartlang.org" 247 | source: hosted 248 | version: "1.8.1" 249 | stack_trace: 250 | dependency: transitive 251 | description: 252 | name: stack_trace 253 | url: "https://pub.dartlang.org" 254 | source: hosted 255 | version: "1.10.0" 256 | stream_channel: 257 | dependency: transitive 258 | description: 259 | name: stream_channel 260 | url: "https://pub.dartlang.org" 261 | source: hosted 262 | version: "2.1.0" 263 | string_scanner: 264 | dependency: transitive 265 | description: 266 | name: string_scanner 267 | url: "https://pub.dartlang.org" 268 | source: hosted 269 | version: "1.1.0" 270 | term_glyph: 271 | dependency: transitive 272 | description: 273 | name: term_glyph 274 | url: "https://pub.dartlang.org" 275 | source: hosted 276 | version: "1.2.0" 277 | test: 278 | dependency: "direct dev" 279 | description: 280 | name: test 281 | url: "https://pub.dartlang.org" 282 | source: hosted 283 | version: "1.17.10" 284 | test_api: 285 | dependency: transitive 286 | description: 287 | name: test_api 288 | url: "https://pub.dartlang.org" 289 | source: hosted 290 | version: "0.4.2" 291 | test_core: 292 | dependency: transitive 293 | description: 294 | name: test_core 295 | url: "https://pub.dartlang.org" 296 | source: hosted 297 | version: "0.4.0" 298 | typed_data: 299 | dependency: transitive 300 | description: 301 | name: typed_data 302 | url: "https://pub.dartlang.org" 303 | source: hosted 304 | version: "1.3.0" 305 | vm_service: 306 | dependency: transitive 307 | description: 308 | name: vm_service 309 | url: "https://pub.dartlang.org" 310 | source: hosted 311 | version: "7.1.1" 312 | watcher: 313 | dependency: transitive 314 | description: 315 | name: watcher 316 | url: "https://pub.dartlang.org" 317 | source: hosted 318 | version: "1.0.0" 319 | web_socket_channel: 320 | dependency: transitive 321 | description: 322 | name: web_socket_channel 323 | url: "https://pub.dartlang.org" 324 | source: hosted 325 | version: "2.1.0" 326 | webkit_inspection_protocol: 327 | dependency: transitive 328 | description: 329 | name: webkit_inspection_protocol 330 | url: "https://pub.dartlang.org" 331 | source: hosted 332 | version: "1.0.0" 333 | yaml: 334 | dependency: transitive 335 | description: 336 | name: yaml 337 | url: "https://pub.dartlang.org" 338 | source: hosted 339 | version: "3.1.0" 340 | sdks: 341 | dart: ">=2.12.0 <3.0.0" 342 | -------------------------------------------------------------------------------- /lib/id3.dart: -------------------------------------------------------------------------------- 1 | library id3; 2 | 3 | import 'dart:convert'; 4 | 5 | import 'src/const.dart'; 6 | 7 | class MP3ParserException implements Exception { 8 | String cause; 9 | MP3ParserException(this.cause); 10 | String toString() { 11 | return this.cause; 12 | } 13 | } 14 | 15 | class _MP3FrameParser { 16 | List buffer; 17 | int pos = 0; 18 | int lastEncoding = 0x00; // default to latin1 19 | _MP3FrameParser(this.buffer); 20 | List readUntilTerminator(List terminator, 21 | {bool aligned = false, bool terminatorMandatory = true}) { 22 | if (remainingBytes == 0) { 23 | return []; 24 | } 25 | for (int i = pos; 26 | i < buffer.length - (terminator.length - 1); 27 | i += (aligned ? terminator.length : 1)) { 28 | bool foundTerminator = true; 29 | for (int j = 0; j < terminator.length; j++) { 30 | if (buffer[i + j] != terminator[j]) { 31 | foundTerminator = false; 32 | break; 33 | } 34 | } 35 | if (foundTerminator) { 36 | final start = pos; 37 | pos = i + terminator.length; 38 | return buffer.sublist(start, pos - terminator.length); 39 | } 40 | } 41 | if (terminatorMandatory) { 42 | throw MP3ParserException( 43 | "Did not find terminator $terminator in ${buffer.sublist(pos)}"); 44 | } else { 45 | return buffer.sublist(pos); 46 | } 47 | } 48 | 49 | String readLatin1String({bool terminator = true}) { 50 | return latin1 51 | .decode(readUntilTerminator([0x00], terminatorMandatory: terminator)); 52 | } 53 | 54 | String readUTF16LEString({bool terminator = true}) { 55 | final bytes = readUntilTerminator([0x00, 0x00], 56 | aligned: true, terminatorMandatory: terminator); 57 | // final utf16les = List((bytes.length / 2).ceil()); 58 | final utf16les = List.generate((bytes.length / 2).ceil(), (index) => 0); 59 | 60 | for (int i = 0; i < bytes.length; i++) { 61 | if (i % 2 == 0) { 62 | utf16les[i ~/ 2] = bytes[i]; 63 | } else { 64 | utf16les[i ~/ 2] |= (bytes[i] << 8); 65 | } 66 | } 67 | return String.fromCharCodes(utf16les); 68 | } 69 | 70 | String readUTF16BEString({bool terminator = true}) { 71 | final bytes = 72 | readUntilTerminator([0x00, 0x00], terminatorMandatory: terminator); 73 | // final utf16bes = List((bytes.length / 2).ceil()); 74 | final utf16bes = List.generate((bytes.length / 2).ceil(), (index) => 0); 75 | 76 | for (int i = 0; i < bytes.length; i++) { 77 | if (i % 2 == 0) { 78 | utf16bes[i ~/ 2] = (bytes[i] << 8); 79 | } else { 80 | utf16bes[i ~/ 2] |= bytes[i]; 81 | } 82 | } 83 | return String.fromCharCodes(utf16bes); 84 | } 85 | 86 | String readUTF16String({bool terminator = true}) { 87 | final bom = buffer.sublist(pos, pos + 2); 88 | if (bom[0] == 0xFF && bom[1] == 0xFE) { 89 | pos += 2; 90 | return readUTF16LEString(terminator: terminator); 91 | } else if (bom[0] == 0xFE && bom[1] == 0xFF) { 92 | pos += 2; 93 | return readUTF16BEString(terminator: terminator); 94 | } else if (bom[0] == 0x00 && bom[1] == 0x00) { 95 | pos += 2; 96 | return ""; 97 | } else { 98 | throw MP3ParserException( 99 | "Unknown UTF-16 BOM: $bom in ${buffer.sublist(pos)}"); 100 | } 101 | } 102 | 103 | String readUTF8String({bool terminator = true}) { 104 | final bytes = readUntilTerminator([0x00], terminatorMandatory: terminator); 105 | return Utf8Decoder().convert(bytes); 106 | } 107 | 108 | void readEncoding() { 109 | if (buffer[pos] < 20) { 110 | if (lastEncoding == 0x01) { 111 | // Do not modify the BOM, 0x01 must apply to each field 112 | pos++; 113 | } else { 114 | lastEncoding = buffer[pos++]; 115 | } 116 | } 117 | } 118 | 119 | String readString({bool terminator = true, bool checkEncoding: true}) { 120 | if (checkEncoding) { 121 | readEncoding(); 122 | } 123 | if (pos == buffer.length) { 124 | return ''; 125 | } 126 | if (lastEncoding == 0x00) { 127 | return readLatin1String(terminator: terminator); 128 | } else if (lastEncoding == 0x01) { 129 | return readUTF16String(terminator: terminator); 130 | } else if (lastEncoding == 0x02) { 131 | return readUTF16BEString(terminator: terminator); 132 | } else if (lastEncoding == 0x03) { 133 | return readUTF8String(terminator: terminator); 134 | } else { 135 | throw MP3ParserException( 136 | "Unknown Byte-Order Marker: $lastEncoding in $buffer"); 137 | } 138 | } 139 | 140 | List readBytes(int length) { 141 | pos += length; 142 | return buffer.sublist(pos - length, pos); 143 | } 144 | 145 | List readRemainingBytes() { 146 | return buffer.sublist(pos); 147 | } 148 | 149 | int get remainingBytes { 150 | return buffer.length - pos; 151 | } 152 | } 153 | 154 | class MP3Instance { 155 | late final List mp3Bytes; 156 | final Map metaTags = {}; 157 | 158 | /// Member Functions 159 | MP3Instance(List mp3Bytes) { 160 | this.mp3Bytes = mp3Bytes; 161 | } 162 | 163 | bool parseTagsSync() { 164 | List _tag; 165 | _tag = mp3Bytes.sublist(0, 3); 166 | 167 | if (latin1.decode(_tag) == 'ID3') { 168 | final int major_v = mp3Bytes[3]; 169 | final int revision_v = mp3Bytes[4]; 170 | final int flag = mp3Bytes[5]; 171 | 172 | final bool unsync = (0x40 & flag != 0); 173 | final bool extended = (0x20 & flag != 0); 174 | final bool experimental = (0x10 & flag != 0); 175 | 176 | metaTags['Version'] = 'v2.$major_v.$revision_v'; 177 | 178 | if (extended) { 179 | print('Extended id3v2 tags are not supported yet!'); 180 | } else if (unsync) { 181 | print('Unsync id3v2 tags are not supported yet!'); 182 | } else if (experimental) { 183 | print('Experimental id3v2 tag'); 184 | } 185 | 186 | int cb = 10; 187 | 188 | Map frames_db = FRAMESv2_3; 189 | int frameNameLength = 4; 190 | int frameSizeLength = 4; 191 | int frameTagLength = 2; 192 | if (major_v == 2) { 193 | frames_db = FRAMESv2_2; 194 | frameNameLength = 3; 195 | frameSizeLength = 3; 196 | frameTagLength = 0; 197 | } 198 | final int frameHeaderLength = 199 | frameNameLength + frameSizeLength + frameTagLength; 200 | 201 | while (true) { 202 | final List frameHeader = 203 | mp3Bytes.sublist(cb, cb + frameHeaderLength); 204 | final List frameName = frameHeader.sublist(0, frameNameLength); 205 | 206 | final RegExp exp = RegExp(r'[A-Z0-9]+'); 207 | if (latin1.decode(frameName) != 208 | exp.stringMatch(latin1.decode(frameName))) { 209 | break; 210 | } 211 | 212 | final int frameSize = parseSize( 213 | frameHeader.sublist( 214 | frameNameLength, frameNameLength + frameSizeLength), 215 | major_v); 216 | final List frameContent = mp3Bytes.sublist( 217 | cb + frameHeaderLength, cb + frameHeaderLength + frameSize); 218 | 219 | if (frames_db[latin1.decode(frameName)] == FRAMESv2_3['APIC']) { 220 | final Map apic = { 221 | 'mime': '', 222 | 'textEncoding': frameContent[0].toString(), 223 | 'picType': '', 224 | 'description': '', 225 | 'base64': '' 226 | }; 227 | 228 | final frame = _MP3FrameParser(frameContent); 229 | frame.readEncoding(); 230 | // In ID3v2.2 the MIME type is always 3 characters 231 | if (major_v == 2) { 232 | apic['mime'] = latin1.decode(frame.readBytes(3)); 233 | } else { 234 | apic['mime'] = frame.readLatin1String(); 235 | } 236 | apic['picType'] = PICTYPE[frame.readBytes(1).first] ?? 'Unknown'; 237 | apic['description'] = frame.readString(checkEncoding: false); 238 | apic['base64'] = base64.encode(frame.readRemainingBytes()); 239 | metaTags['APIC'] = apic; 240 | } else if (frames_db[latin1.decode(frameName)] == FRAMESv2_3['USLT']) { 241 | final frame = _MP3FrameParser(frameContent); 242 | frame.readEncoding(); 243 | final language = latin1.decode(frame.readBytes(3)); 244 | String contentDescriptor; 245 | contentDescriptor = frame.readString(checkEncoding: false); 246 | final lyrics = (frame.remainingBytes > 0) 247 | ? frame.readString(checkEncoding: false, terminator: false) 248 | : contentDescriptor; 249 | if (frame.remainingBytes == 0) { 250 | contentDescriptor = ''; 251 | } 252 | metaTags['USLT'] = { 253 | 'language': language, 254 | 'contentDescriptor': contentDescriptor, 255 | 'lyrics': lyrics 256 | }; 257 | } else if (frames_db[latin1.decode(frameName)] == FRAMESv2_3['WXXX']) { 258 | final frame = _MP3FrameParser(frameContent); 259 | metaTags['WXXX'] = { 260 | 'description': frame.readString(), 261 | 'url': frame.readLatin1String(terminator: false) 262 | }; 263 | } else if (frames_db[latin1.decode(frameName)] == FRAMESv2_3['COMM']) { 264 | final frame = _MP3FrameParser(frameContent); 265 | frame.readEncoding(); 266 | final language = latin1.decode(frame.readBytes(3)); 267 | final shortDescription = frame.readString(checkEncoding: false); 268 | final text = 269 | frame.readString(terminator: false, checkEncoding: false); 270 | if (metaTags['COMM'] == null) { 271 | metaTags['COMM'] = {}; 272 | if (metaTags['COMM'][language] == null) { 273 | metaTags['COMM'][language] = {}; 274 | } 275 | } 276 | metaTags['COMM'][language][shortDescription] = text; 277 | } else if (frames_db[latin1.decode(frameName)] == FRAMESv2_3['MCDI'] || 278 | frames_db[latin1.decode(frameName)] == FRAMESv2_3['RVAD']) { 279 | // Binary data 280 | metaTags[frames_db[latin1.decode(frameName)] ?? 281 | latin1.decode(frameName)] = frameContent; 282 | } else { 283 | final String tag = 284 | frames_db[latin1.decode(frameName)] ?? latin1.decode(frameName); 285 | metaTags[tag] = 286 | _MP3FrameParser(frameContent).readString(terminator: false); 287 | } 288 | 289 | cb += frameHeaderLength + frameSize; 290 | } 291 | 292 | return true; 293 | } 294 | 295 | final List _header = 296 | mp3Bytes.sublist(mp3Bytes.length - 128, mp3Bytes.length); 297 | _tag = _header.sublist(0, 3); 298 | 299 | if (latin1.decode(_tag).toLowerCase() == 'tag') { 300 | metaTags['Version'] = '1.0'; 301 | 302 | final List _title = _header.sublist(3, 33); 303 | final List _artist = _header.sublist(33, 63); 304 | final List _album = _header.sublist(63, 93); 305 | final List _year = _header.sublist(93, 97); 306 | final List _comment = _header.sublist(97, 127); 307 | final int _genre = _header[127]; 308 | 309 | metaTags['Title'] = latin1.decode(_title).trim(); 310 | metaTags['Artist'] = latin1.decode(_artist).trim(); 311 | metaTags['Album'] = latin1.decode(_album).trim(); 312 | metaTags['Year'] = latin1.decode(_year).trim(); 313 | metaTags['Comment'] = latin1.decode(_comment).trim(); 314 | metaTags['Genre'] = GENREv1[_genre]; 315 | 316 | return true; 317 | } 318 | 319 | return false; 320 | } 321 | 322 | Map? getMetaTags() { 323 | return metaTags; 324 | } 325 | } 326 | 327 | int parseSize(List block, int major_v) { 328 | int len; 329 | if (major_v == 4) { 330 | assert(block.length == 4); 331 | len = block[0] << 21; 332 | len += block[1] << 14; 333 | len += block[2] << 7; 334 | len += block[3]; 335 | } else if (major_v == 3) { 336 | assert(block.length == 4); 337 | len = block[0] << 24; 338 | len += block[1] << 16; 339 | len += block[2] << 8; 340 | len += block[3]; 341 | } else if (major_v == 2) { 342 | assert(block.length == 3); 343 | len = block[0] << 16; 344 | len += block[1] << 8; 345 | len += block[2]; 346 | } else { 347 | throw MP3ParserException("Unknown major version $major_v"); 348 | } 349 | 350 | return len; 351 | } 352 | 353 | List cleanFrame(List bytes) { 354 | List temp = new List.from(bytes); 355 | 356 | temp.removeWhere((item) => item < 1); 357 | 358 | if (temp.length > 3) { 359 | return temp.sublist(3); 360 | } else { 361 | return temp; 362 | } 363 | } 364 | 365 | List removeZeros(List bytes) { 366 | return bytes.where((i) => i != 0).toList(); 367 | } 368 | --------------------------------------------------------------------------------