From 9ce4f4ba4fbb4ec9b5f26a744fa039dfb24ef5c6 Mon Sep 17 00:00:00 2001 From: Hitesh Kumar Saini Date: Sun, 30 Jan 2022 21:26:52 +0530 Subject: [PATCH] Plugin: Add web support & improve structure --- lib/flutter_media_metadata.dart | 153 ++++------------- lib/src/flutter_media_metadata_native.dart | 56 +++++++ lib/src/flutter_media_metadata_web.dart | 182 +++++++++++++++++++++ lib/src/models/metadata.dart | 121 ++++++++++++++ lib/src/utils.dart | 24 +++ 5 files changed, 415 insertions(+), 121 deletions(-) create mode 100644 lib/src/flutter_media_metadata_native.dart create mode 100644 lib/src/flutter_media_metadata_web.dart create mode 100644 lib/src/models/metadata.dart create mode 100644 lib/src/utils.dart diff --git a/lib/flutter_media_metadata.dart b/lib/flutter_media_metadata.dart index 35b7d76..405776e 100644 --- a/lib/flutter_media_metadata.dart +++ b/lib/flutter_media_metadata.dart @@ -1,121 +1,32 @@ -import 'dart:io'; -import 'dart:convert'; -import 'dart:typed_data'; -import 'package:flutter/services.dart'; - -var _kChannel = const MethodChannel('flutter_media_metadata'); - -class MetadataRetriever { - static Future fromFile( - File file, { - bool createNewInstance: false, - }) async { - var metadata = await _kChannel.invokeMethod( - 'MetadataRetriever', - { - 'filePath': file.path, - 'createNewInstance': createNewInstance, - }, - ); - metadata['filePath'] = file.path; - return Metadata.fromJson(metadata); - } -} - -class Metadata { - final String? trackName; - final List? trackArtistNames; - final String? albumName; - final String? albumArtistName; - final int? trackNumber; - final int? albumLength; - final int? year; - final String? genre; - final String? authorName; - final String? writerName; - final int? discNumber; - final String? mimeType; - final int? trackDuration; - final int? bitrate; - final Uint8List? albumArt; - final String filePath; - - const Metadata({ - this.trackName, - this.trackArtistNames, - this.albumName, - this.albumArtistName, - this.trackNumber, - this.albumLength, - this.year, - this.genre, - this.authorName, - this.writerName, - this.discNumber, - this.mimeType, - this.trackDuration, - this.bitrate, - this.albumArt, - required this.filePath, - }); - - factory Metadata.fromJson(dynamic map) => Metadata( - trackName: map['metadata']['trackName'], - trackArtistNames: map['metadata']['trackArtistNames'] != null - ? map['metadata']['trackArtistNames'].split('/') - : null, - albumName: map['metadata']['albumName'], - albumArtistName: map['metadata']['albumArtistName'], - trackNumber: _parse(map['metadata']['trackNumber']), - albumLength: _parse(map['metadata']['albumLength']), - year: _parse(map['metadata']['year']), - genre: map['genre'], - authorName: map['metadata']['authorName'], - writerName: map['metadata']['writerName'], - discNumber: _parse(map['metadata']['discNumber']), - mimeType: map['metadata']['mimeType'], - trackDuration: _parse(map['metadata']['trackDuration']), - bitrate: _parse(map['metadata']['bitrate']), - albumArt: map['albumArt'], - filePath: map['filePath'], - ); - - Map toJson() => { - 'trackName': trackName, - 'trackArtistNames': trackArtistNames, - 'albumName': albumName, - 'albumArtistName': albumArtistName, - 'trackNumber': trackNumber, - 'albumLength': albumLength, - 'year': year, - 'genre': genre, - 'authorName': authorName, - 'writerName': writerName, - 'discNumber': discNumber, - 'mimeType': mimeType, - 'trackDuration': trackDuration, - 'bitrate': bitrate, - 'filePath': filePath, - }; - - @override - String toString() => JsonEncoder.withIndent(' ').convert(toJson()); -} - -int? _parse(dynamic value) { - if (value == null) { - return null; - } - if (value is int) { - return value; - } else if (value is String) { - try { - try { - return int.parse(value); - } catch (_) { - return int.parse(value.split('/').first); - } - } catch (_) {} - } - return null; -} +/// ## flutter_media_metadata +/// +/// A Flutter plugin to read metadata of media files. +/// +/// MIT License. +/// Copyright (c) 2021-2022, Hitesh Kumar Saini . +/// +/// _Minimal Example_ +/// ```dart +/// final metadata = MetadataRetriever.fromBytes(byteData); +/// String? trackName = metadata.trackName; +/// List? trackArtistNames = metadata.trackArtistNames; +/// String? albumName = metadata.albumName; +/// String? albumArtistName = metadata.albumArtistName; +/// int? trackNumber = metadata.trackNumber; +/// int? albumLength = metadata.albumLength; +/// int? year = metadata.year; +/// String? genre = metadata.genre; +/// String? authorName = metadata.authorName; +/// String? writerName = metadata.writerName; +/// int? discNumber = metadata.discNumber; +/// String? mimeType = metadata.mimeType; +/// int? trackDuration = metadata.trackDuration; +/// int? bitrate = metadata.bitrate; +/// Uint8List? albumArt = metadata.albumArt; +/// ``` +/// +library flutter_media_metadata; + +export 'package:flutter_media_metadata/src/flutter_media_metadata_native.dart' + if (dart.library.html) 'package:flutter_media_metadata/src/flutter_media_metadata_web.dart'; +export 'package:flutter_media_metadata/src/models/metadata.dart'; diff --git a/lib/src/flutter_media_metadata_native.dart b/lib/src/flutter_media_metadata_native.dart new file mode 100644 index 0000000..4735643 --- /dev/null +++ b/lib/src/flutter_media_metadata_native.dart @@ -0,0 +1,56 @@ +/// This file is a part of flutter_media_metadata (https://github.com/alexmercerind/flutter_media_metadata). +/// +/// Copyright (c) 2021-2022, Hitesh Kumar Saini . +/// All rights reserved. +/// Use of this source code is governed by MIT license that can be found in the LICENSE file. + +import 'dart:io'; +import 'package:flutter/services.dart'; + +import 'package:flutter_media_metadata/src/models/metadata.dart'; + +/// ## MetadataRetriever +/// +/// Use [MetadataRetriever.fromFile] to extract [Metadata] from a media file. +/// +/// ```dart +/// final metadata = MetadataRetriever.fromFile(file); +/// String? trackName = metadata.trackName; +/// List? trackArtistNames = metadata.trackArtistNames; +/// String? albumName = metadata.albumName; +/// String? albumArtistName = metadata.albumArtistName; +/// int? trackNumber = metadata.trackNumber; +/// int? albumLength = metadata.albumLength; +/// int? year = metadata.year; +/// String? genre = metadata.genre; +/// String? authorName = metadata.authorName; +/// String? writerName = metadata.writerName; +/// int? discNumber = metadata.discNumber; +/// String? mimeType = metadata.mimeType; +/// int? trackDuration = metadata.trackDuration; +/// int? bitrate = metadata.bitrate; +/// Uint8List? albumArt = metadata.albumArt; +/// ``` +/// +class MetadataRetriever { + /// Extracts [Metadata] from a [File]. Works on Windows, Linux, macOS, Android & iOS. + static Future fromFile(File file) async { + var metadata = await _kChannel.invokeMethod( + 'MetadataRetriever', + { + 'filePath': file.path, + }, + ); + metadata['filePath'] = file.path; + return Metadata.fromJson(metadata); + } + + /// Extracts [Metadata] from [Uint8List]. Works only on Web. + static Future fromBytes(dynamic _) async { + throw UnimplementedError( + '[MetadataRetriever.fromBytes] is not supported on ${Platform.operatingSystem}. This method is only available for web. Use [MetadataRetriever.fromFile] instead.', + ); + } +} + +var _kChannel = const MethodChannel('flutter_media_metadata'); diff --git a/lib/src/flutter_media_metadata_web.dart b/lib/src/flutter_media_metadata_web.dart new file mode 100644 index 0000000..bb1cdbc --- /dev/null +++ b/lib/src/flutter_media_metadata_web.dart @@ -0,0 +1,182 @@ +/// This file is a part of flutter_media_metadata (https://github.com/alexmercerind/flutter_media_metadata). +/// +/// Copyright (c) 2021-2022, Hitesh Kumar Saini . +/// All rights reserved. +/// Use of this source code is governed by MIT license that can be found in the LICENSE file. + +// ignore_for_file: missing_js_lib_annotation + +import 'dart:async'; +import 'dart:convert'; +import 'dart:typed_data'; +import 'package:js/js.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_web_plugins/flutter_web_plugins.dart'; + +import 'package:flutter_media_metadata/src/models/metadata.dart'; + +/// ## MetadataRetriever +/// +/// Use [MetadataRetriever.fromBytes] to extract [Metadata] from bytes of media file. +/// +/// ```dart +/// final metadata = MetadataRetriever.fromBytes(byteData); +/// String? trackName = metadata.trackName; +/// List? trackArtistNames = metadata.trackArtistNames; +/// String? albumName = metadata.albumName; +/// String? albumArtistName = metadata.albumArtistName; +/// int? trackNumber = metadata.trackNumber; +/// int? albumLength = metadata.albumLength; +/// int? year = metadata.year; +/// String? genre = metadata.genre; +/// String? authorName = metadata.authorName; +/// String? writerName = metadata.writerName; +/// int? discNumber = metadata.discNumber; +/// String? mimeType = metadata.mimeType; +/// int? trackDuration = metadata.trackDuration; +/// int? bitrate = metadata.bitrate; +/// Uint8List? albumArt = metadata.albumArt; +/// ``` +/// +class MetadataRetriever { + static void registerWith(Registrar registrar) { + final MethodChannel channel = MethodChannel( + 'flutter_media_metadata', + const StandardMethodCodec(), + registrar, + ); + final pluginInstance = MetadataRetriever(); + channel.setMethodCallHandler(pluginInstance.handleMethodCall); + } + + Future handleMethodCall(MethodCall call) => throw PlatformException( + code: 'Unimplemented', + details: + 'flutter_media_metadata for web doesn\'t implement \'${call.method}\'', + ); + + /// Extracts [Metadata] from a [File]. Works on Windows, Linux, macOS, Android & iOS. + static Future fromFile(dynamic _) async { + throw UnimplementedError( + '[MetadataRetriever.fromFile] is not supported on web. This method is only available for Windows, Linux, macOS, Android or iOS. Use [MetadataRetriever.fromBytes] instead.', + ); + } + + /// Extracts [Metadata] from [Uint8List]. Works only on Web. + static Future fromBytes(Uint8List bytes) { + final completer = Completer(); + MediaInfo( + _Opts( + chunkSize: 256 * 1024, + coverData: true, + format: 'JSON', + ), + allowInterop( + (mediainfo) { + mediainfo + .analyzeData( + allowInterop(() => bytes.length), + allowInterop( + (chunkSize, offset) => _Promise( + allowInterop( + (resolve, reject) { + resolve( + bytes.sublist( + offset, + offset + chunkSize, + ), + ); + }, + ), + ), + ), + ) + .then( + allowInterop( + (result) { + var rawMetadataJson = jsonDecode(result)['media']['track']; + bool isFound = false; + for (final data in rawMetadataJson) { + if (data['@type'] == 'General') { + isFound = true; + rawMetadataJson = data; + break; + } + } + if (!isFound) { + throw Exception(); + } + final metadata = { + 'metadata': {}, + 'albumArt': base64Decode(rawMetadataJson['Cover_Data']), + 'filePath': null, + }; + _kMetadataKeys.forEach((key, value) { + metadata['metadata'][key] = rawMetadataJson[value]; + }); + completer.complete(Metadata.fromJson(metadata)); + }, + ), + allowInterop( + () { + completer.completeError(Exception()); + }, + ), + ); + }, + ), + allowInterop( + () { + completer.completeError(Exception()); + }, + ), + ); + return completer.future; + } +} + +@JS('Promise') +class _Promise { + external _Promise(void executor(void resolve(T result), Function reject)); + external _Promise then(void onFulfilled(T result), [Function onRejected]); +} + +@JS('MediaInfo') +external String MediaInfo( + Object opts, + void Function(_MediaInfo) successCallback, + void Function() erroCallback, +); + +@JS() +@anonymous +class _Opts { + external int chunkSize; + external bool coverData; + external String format; + + external factory _Opts({int chunkSize, bool coverData, String format}); +} + +@JS() +@anonymous +class _MediaInfo { + external _Promise analyzeData(int Function() getSize, + _Promise promise(int chunkSize, int offset)); + + external factory _MediaInfo(); +} + +const _kMetadataKeys = { + "trackName": "Track", + "trackArtistNames": "Performer", + "albumName": "Album", + "albumArtistName": "Album_Performer", + "trackNumber": "Track_Position", + "albumLength": "Track_Position_Total", + "year": "Recorded_Date", + "genre": "Genre", + "writerName": "WrittenBy", + "trackDuration": "Duration", + "bitrate": "OverallBitRate", +}; diff --git a/lib/src/models/metadata.dart b/lib/src/models/metadata.dart new file mode 100644 index 0000000..0733189 --- /dev/null +++ b/lib/src/models/metadata.dart @@ -0,0 +1,121 @@ +/// This file is a part of flutter_media_metadata (https://github.com/alexmercerind/flutter_media_metadata). +/// +/// Copyright (c) 2021-2022, Hitesh Kumar Saini . +/// All rights reserved. +/// Use of this source code is governed by MIT license that can be found in the LICENSE file. + +import 'dart:typed_data'; + +import 'package:flutter_media_metadata/src/utils.dart'; + +/// Metadata of a media file. +class Metadata { + /// Name of the track. + final String? trackName; + + /// Names of the artists performing in the track. + final List? trackArtistNames; + + /// Name of the album. + final String? albumName; + + /// Name of the album artist. + final String? albumArtistName; + + /// Position of track in the album. + final int? trackNumber; + + /// Number of tracks in the album. + final int? albumLength; + + /// Year of the track. + final int? year; + + /// Genre of the track. + final String? genre; + + /// Author of the track. + final String? authorName; + + /// Writer of the track. + final String? writerName; + + /// Number of the disc. + final int? discNumber; + + /// Mime type. + final String? mimeType; + + /// Duration of the track in milliseconds. + final int? trackDuration; + + /// Bitrate of the track. + final int? bitrate; + + /// [Uint8List] having album art data. + final Uint8List? albumArt; + + /// File path of the media file. `null` on web. + final String? filePath; + + const Metadata({ + this.trackName, + this.trackArtistNames, + this.albumName, + this.albumArtistName, + this.trackNumber, + this.albumLength, + this.year, + this.genre, + this.authorName, + this.writerName, + this.discNumber, + this.mimeType, + this.trackDuration, + this.bitrate, + this.albumArt, + this.filePath, + }); + + factory Metadata.fromJson(dynamic map) => Metadata( + trackName: map['metadata']['trackName'], + trackArtistNames: map['metadata']['trackArtistNames'] != null + ? map['metadata']['trackArtistNames'].split('/') + : null, + albumName: map['metadata']['albumName'], + albumArtistName: map['metadata']['albumArtistName'], + trackNumber: parseInteger(map['metadata']['trackNumber']), + albumLength: parseInteger(map['metadata']['albumLength']), + year: parseInteger(map['metadata']['year']), + genre: map['genre'], + authorName: map['metadata']['authorName'], + writerName: map['metadata']['writerName'], + discNumber: parseInteger(map['metadata']['discNumber']), + mimeType: map['metadata']['mimeType'], + trackDuration: parseInteger(map['metadata']['trackDuration']), + bitrate: parseInteger(map['metadata']['bitrate']), + albumArt: map['albumArt'], + filePath: map['filePath'], + ); + + Map toJson() => { + 'trackName': trackName, + 'trackArtistNames': trackArtistNames, + 'albumName': albumName, + 'albumArtistName': albumArtistName, + 'trackNumber': trackNumber, + 'albumLength': albumLength, + 'year': year, + 'genre': genre, + 'authorName': authorName, + 'writerName': writerName, + 'discNumber': discNumber, + 'mimeType': mimeType, + 'trackDuration': trackDuration, + 'bitrate': bitrate, + 'filePath': filePath, + }; + + @override + String toString() => toJson().toString(); +} diff --git a/lib/src/utils.dart b/lib/src/utils.dart new file mode 100644 index 0000000..7b2fc81 --- /dev/null +++ b/lib/src/utils.dart @@ -0,0 +1,24 @@ +/// This file is a part of flutter_media_metadata (https://github.com/alexmercerind/flutter_media_metadata). +/// +/// Copyright (c) 2021-2022, Hitesh Kumar Saini . +/// All rights reserved. +/// Use of this source code is governed by MIT license that can be found in the LICENSE file. + +/// Safely parses [int] from a [String]. +int? parseInteger(dynamic value) { + if (value == null) { + return null; + } + if (value is int) { + return value; + } else if (value is String) { + try { + try { + return int.parse(value); + } catch (_) { + return int.parse(value.split('/').first); + } + } catch (_) {} + } + return null; +}