mirror of
https://github.com/KRTirtho/spotube.git
synced 2025-12-05 23:19:42 +00:00
feat: add plugin audio source models and api service
This commit is contained in:
parent
88699e9a3b
commit
439de5d7f7
84
lib/models/metadata/audio_source.dart
Normal file
84
lib/models/metadata/audio_source.dart
Normal file
@ -0,0 +1,84 @@
|
||||
part of 'metadata.dart';
|
||||
|
||||
enum SpotubeMediaCompressionType {
|
||||
lossy,
|
||||
lossless,
|
||||
}
|
||||
|
||||
@Freezed(unionKey: 'type')
|
||||
class SpotubeAudioSourceContainerPreset
|
||||
with _$SpotubeAudioSourceContainerPreset {
|
||||
@FreezedUnionValue("lossy")
|
||||
factory SpotubeAudioSourceContainerPreset.lossy({
|
||||
required SpotubeMediaCompressionType type,
|
||||
required String name,
|
||||
required List<SpotubeAudioLossyContainerQuality> qualities,
|
||||
}) = SpotubeAudioSourceContainerPresetLossy;
|
||||
|
||||
@FreezedUnionValue("lossless")
|
||||
factory SpotubeAudioSourceContainerPreset.lossless({
|
||||
required SpotubeMediaCompressionType type,
|
||||
required String name,
|
||||
required List<SpotubeAudioLosslessContainerQuality> qualities,
|
||||
}) = SpotubeAudioSourceContainerPresetLossless;
|
||||
|
||||
factory SpotubeAudioSourceContainerPreset.fromJson(
|
||||
Map<String, dynamic> json) =>
|
||||
_$SpotubeAudioSourceContainerPresetFromJson(json);
|
||||
}
|
||||
|
||||
@freezed
|
||||
class SpotubeAudioLossyContainerQuality
|
||||
with _$SpotubeAudioLossyContainerQuality {
|
||||
factory SpotubeAudioLossyContainerQuality({
|
||||
required double bitrate,
|
||||
}) = _SpotubeAudioLossyContainerQuality;
|
||||
|
||||
factory SpotubeAudioLossyContainerQuality.fromJson(
|
||||
Map<String, dynamic> json) =>
|
||||
_$SpotubeAudioLossyContainerQualityFromJson(json);
|
||||
}
|
||||
|
||||
@freezed
|
||||
class SpotubeAudioLosslessContainerQuality
|
||||
with _$SpotubeAudioLosslessContainerQuality {
|
||||
factory SpotubeAudioLosslessContainerQuality({
|
||||
required int bitDepth,
|
||||
required double sampleRate,
|
||||
}) = _SpotubeAudioLosslessContainerQuality;
|
||||
|
||||
factory SpotubeAudioLosslessContainerQuality.fromJson(
|
||||
Map<String, dynamic> json) =>
|
||||
_$SpotubeAudioLosslessContainerQualityFromJson(json);
|
||||
}
|
||||
|
||||
@freezed
|
||||
class SpotubeAudioSourceMatchObject with _$SpotubeAudioSourceMatchObject {
|
||||
factory SpotubeAudioSourceMatchObject({
|
||||
required String id,
|
||||
required String title,
|
||||
required List<String> artists,
|
||||
required Duration duration,
|
||||
String? thumbnail,
|
||||
required String externalUri,
|
||||
}) = _SpotubeAudioSourceMatchObject;
|
||||
|
||||
factory SpotubeAudioSourceMatchObject.fromJson(Map<String, dynamic> json) =>
|
||||
_$SpotubeAudioSourceMatchObjectFromJson(json);
|
||||
}
|
||||
|
||||
@freezed
|
||||
class SpotubeAudioSourceStreamObject with _$SpotubeAudioSourceStreamObject {
|
||||
factory SpotubeAudioSourceStreamObject({
|
||||
required String url,
|
||||
required String container,
|
||||
required SpotubeMediaCompressionType type,
|
||||
String? codec,
|
||||
double? bitrate,
|
||||
int? bitDepth,
|
||||
double? sampleRate,
|
||||
}) = _SpotubeAudioSourceStreamObject;
|
||||
|
||||
factory SpotubeAudioSourceStreamObject.fromJson(Map<String, dynamic> json) =>
|
||||
_$SpotubeAudioSourceStreamObjectFromJson(json);
|
||||
}
|
||||
@ -15,6 +15,7 @@ import 'package:spotube/utils/primitive_utils.dart';
|
||||
part 'metadata.g.dart';
|
||||
part 'metadata.freezed.dart';
|
||||
|
||||
part 'audio_source.dart';
|
||||
part 'album.dart';
|
||||
part 'artist.dart';
|
||||
part 'browse.dart';
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -6,6 +6,123 @@ part of 'metadata.dart';
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
_$SpotubeAudioSourceContainerPresetLossyImpl
|
||||
_$$SpotubeAudioSourceContainerPresetLossyImplFromJson(Map json) =>
|
||||
_$SpotubeAudioSourceContainerPresetLossyImpl(
|
||||
type: $enumDecode(_$SpotubeMediaCompressionTypeEnumMap, json['type']),
|
||||
name: json['name'] as String,
|
||||
qualities: (json['qualities'] as List<dynamic>)
|
||||
.map((e) => SpotubeAudioLossyContainerQuality.fromJson(
|
||||
Map<String, dynamic>.from(e as Map)))
|
||||
.toList(),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$$SpotubeAudioSourceContainerPresetLossyImplToJson(
|
||||
_$SpotubeAudioSourceContainerPresetLossyImpl instance) =>
|
||||
<String, dynamic>{
|
||||
'type': _$SpotubeMediaCompressionTypeEnumMap[instance.type]!,
|
||||
'name': instance.name,
|
||||
'qualities': instance.qualities.map((e) => e.toJson()).toList(),
|
||||
};
|
||||
|
||||
const _$SpotubeMediaCompressionTypeEnumMap = {
|
||||
SpotubeMediaCompressionType.lossy: 'lossy',
|
||||
SpotubeMediaCompressionType.lossless: 'lossless',
|
||||
};
|
||||
|
||||
_$SpotubeAudioSourceContainerPresetLosslessImpl
|
||||
_$$SpotubeAudioSourceContainerPresetLosslessImplFromJson(Map json) =>
|
||||
_$SpotubeAudioSourceContainerPresetLosslessImpl(
|
||||
type: $enumDecode(_$SpotubeMediaCompressionTypeEnumMap, json['type']),
|
||||
name: json['name'] as String,
|
||||
qualities: (json['qualities'] as List<dynamic>)
|
||||
.map((e) => SpotubeAudioLosslessContainerQuality.fromJson(
|
||||
Map<String, dynamic>.from(e as Map)))
|
||||
.toList(),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$$SpotubeAudioSourceContainerPresetLosslessImplToJson(
|
||||
_$SpotubeAudioSourceContainerPresetLosslessImpl instance) =>
|
||||
<String, dynamic>{
|
||||
'type': _$SpotubeMediaCompressionTypeEnumMap[instance.type]!,
|
||||
'name': instance.name,
|
||||
'qualities': instance.qualities.map((e) => e.toJson()).toList(),
|
||||
};
|
||||
|
||||
_$SpotubeAudioLossyContainerQualityImpl
|
||||
_$$SpotubeAudioLossyContainerQualityImplFromJson(Map json) =>
|
||||
_$SpotubeAudioLossyContainerQualityImpl(
|
||||
bitrate: (json['bitrate'] as num).toDouble(),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$$SpotubeAudioLossyContainerQualityImplToJson(
|
||||
_$SpotubeAudioLossyContainerQualityImpl instance) =>
|
||||
<String, dynamic>{
|
||||
'bitrate': instance.bitrate,
|
||||
};
|
||||
|
||||
_$SpotubeAudioLosslessContainerQualityImpl
|
||||
_$$SpotubeAudioLosslessContainerQualityImplFromJson(Map json) =>
|
||||
_$SpotubeAudioLosslessContainerQualityImpl(
|
||||
bitDepth: (json['bitDepth'] as num).toInt(),
|
||||
sampleRate: (json['sampleRate'] as num).toDouble(),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$$SpotubeAudioLosslessContainerQualityImplToJson(
|
||||
_$SpotubeAudioLosslessContainerQualityImpl instance) =>
|
||||
<String, dynamic>{
|
||||
'bitDepth': instance.bitDepth,
|
||||
'sampleRate': instance.sampleRate,
|
||||
};
|
||||
|
||||
_$SpotubeAudioSourceMatchObjectImpl
|
||||
_$$SpotubeAudioSourceMatchObjectImplFromJson(Map json) =>
|
||||
_$SpotubeAudioSourceMatchObjectImpl(
|
||||
id: json['id'] as String,
|
||||
title: json['title'] as String,
|
||||
artists: (json['artists'] as List<dynamic>)
|
||||
.map((e) => e as String)
|
||||
.toList(),
|
||||
duration: Duration(microseconds: (json['duration'] as num).toInt()),
|
||||
thumbnail: json['thumbnail'] as String?,
|
||||
externalUri: json['externalUri'] as String,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$$SpotubeAudioSourceMatchObjectImplToJson(
|
||||
_$SpotubeAudioSourceMatchObjectImpl instance) =>
|
||||
<String, dynamic>{
|
||||
'id': instance.id,
|
||||
'title': instance.title,
|
||||
'artists': instance.artists,
|
||||
'duration': instance.duration.inMicroseconds,
|
||||
'thumbnail': instance.thumbnail,
|
||||
'externalUri': instance.externalUri,
|
||||
};
|
||||
|
||||
_$SpotubeAudioSourceStreamObjectImpl
|
||||
_$$SpotubeAudioSourceStreamObjectImplFromJson(Map json) =>
|
||||
_$SpotubeAudioSourceStreamObjectImpl(
|
||||
url: json['url'] as String,
|
||||
container: json['container'] as String,
|
||||
type: $enumDecode(_$SpotubeMediaCompressionTypeEnumMap, json['type']),
|
||||
codec: json['codec'] as String?,
|
||||
bitrate: (json['bitrate'] as num?)?.toDouble(),
|
||||
bitDepth: (json['bitDepth'] as num?)?.toInt(),
|
||||
sampleRate: (json['sampleRate'] as num?)?.toDouble(),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$$SpotubeAudioSourceStreamObjectImplToJson(
|
||||
_$SpotubeAudioSourceStreamObjectImpl instance) =>
|
||||
<String, dynamic>{
|
||||
'url': instance.url,
|
||||
'container': instance.container,
|
||||
'type': _$SpotubeMediaCompressionTypeEnumMap[instance.type]!,
|
||||
'codec': instance.codec,
|
||||
'bitrate': instance.bitrate,
|
||||
'bitDepth': instance.bitDepth,
|
||||
'sampleRate': instance.sampleRate,
|
||||
};
|
||||
|
||||
_$SpotubeFullAlbumObjectImpl _$$SpotubeFullAlbumObjectImplFromJson(Map json) =>
|
||||
_$SpotubeFullAlbumObjectImpl(
|
||||
id: json['id'] as String,
|
||||
@ -419,7 +536,6 @@ Map<String, dynamic> _$$SpotubeUserObjectImplToJson(
|
||||
|
||||
_$PluginConfigurationImpl _$$PluginConfigurationImplFromJson(Map json) =>
|
||||
_$PluginConfigurationImpl(
|
||||
type: $enumDecode(_$PluginTypeEnumMap, json['type']),
|
||||
name: json['name'] as String,
|
||||
description: json['description'] as String,
|
||||
version: json['version'] as String,
|
||||
@ -440,7 +556,6 @@ _$PluginConfigurationImpl _$$PluginConfigurationImplFromJson(Map json) =>
|
||||
Map<String, dynamic> _$$PluginConfigurationImplToJson(
|
||||
_$PluginConfigurationImpl instance) =>
|
||||
<String, dynamic>{
|
||||
'type': _$PluginTypeEnumMap[instance.type]!,
|
||||
'name': instance.name,
|
||||
'description': instance.description,
|
||||
'version': instance.version,
|
||||
@ -453,10 +568,6 @@ Map<String, dynamic> _$$PluginConfigurationImplToJson(
|
||||
'repository': instance.repository,
|
||||
};
|
||||
|
||||
const _$PluginTypeEnumMap = {
|
||||
PluginType.metadata: 'metadata',
|
||||
};
|
||||
|
||||
const _$PluginApisEnumMap = {
|
||||
PluginApis.webview: 'webview',
|
||||
PluginApis.localstorage: 'localstorage',
|
||||
@ -466,6 +577,8 @@ const _$PluginApisEnumMap = {
|
||||
const _$PluginAbilitiesEnumMap = {
|
||||
PluginAbilities.authentication: 'authentication',
|
||||
PluginAbilities.scrobbling: 'scrobbling',
|
||||
PluginAbilities.metadata: 'metadata',
|
||||
PluginAbilities.audioSource: 'audio-source',
|
||||
};
|
||||
|
||||
_$PluginUpdateAvailableImpl _$$PluginUpdateAvailableImplFromJson(Map json) =>
|
||||
|
||||
@ -1,17 +1,20 @@
|
||||
part of 'metadata.dart';
|
||||
|
||||
enum PluginType { metadata }
|
||||
|
||||
enum PluginApis { webview, localstorage, timezone }
|
||||
|
||||
enum PluginAbilities { authentication, scrobbling }
|
||||
enum PluginAbilities {
|
||||
authentication,
|
||||
scrobbling,
|
||||
metadata,
|
||||
@JsonValue('audio-source')
|
||||
audioSource,
|
||||
}
|
||||
|
||||
@freezed
|
||||
class PluginConfiguration with _$PluginConfiguration {
|
||||
const PluginConfiguration._();
|
||||
|
||||
factory PluginConfiguration({
|
||||
required PluginType type,
|
||||
required String name,
|
||||
required String description,
|
||||
required String version,
|
||||
|
||||
@ -10,6 +10,7 @@ import 'package:path_provider/path_provider.dart';
|
||||
import 'package:spotube/models/database/database.dart';
|
||||
import 'package:spotube/models/metadata/metadata.dart';
|
||||
import 'package:spotube/provider/database/database.dart';
|
||||
import 'package:spotube/provider/youtube_engine/youtube_engine.dart';
|
||||
import 'package:spotube/services/dio/dio.dart';
|
||||
import 'package:spotube/services/logger/logger.dart';
|
||||
import 'package:spotube/services/metadata/errors/exceptions.dart';
|
||||
@ -97,7 +98,6 @@ class MetadataPluginNotifier extends AsyncNotifier<MetadataPluginState> {
|
||||
final plugin = plugins[i];
|
||||
|
||||
final pluginConfig = PluginConfiguration(
|
||||
type: PluginType.metadata,
|
||||
name: plugin.name,
|
||||
author: plugin.author,
|
||||
description: plugin.description,
|
||||
@ -447,6 +447,7 @@ final metadataPluginProvider = FutureProvider<MetadataPlugin?>(
|
||||
final defaultPlugin = await ref.watch(
|
||||
metadataPluginsProvider.selectAsync((data) => data.defaultPluginConfig),
|
||||
);
|
||||
final youtubeEngine = ref.read(youtubeEngineProvider);
|
||||
|
||||
if (defaultPlugin == null) {
|
||||
return null;
|
||||
@ -456,6 +457,10 @@ final metadataPluginProvider = FutureProvider<MetadataPlugin?>(
|
||||
final pluginByteCode =
|
||||
await pluginsNotifier.getPluginByteCode(defaultPlugin);
|
||||
|
||||
return await MetadataPlugin.create(defaultPlugin, pluginByteCode);
|
||||
return await MetadataPlugin.create(
|
||||
youtubeEngine,
|
||||
defaultPlugin,
|
||||
pluginByteCode,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
38
lib/services/metadata/endpoints/audio_source.dart
Normal file
38
lib/services/metadata/endpoints/audio_source.dart
Normal file
@ -0,0 +1,38 @@
|
||||
import 'package:hetu_script/hetu_script.dart';
|
||||
import 'package:hetu_script/values.dart';
|
||||
import 'package:spotube/models/metadata/metadata.dart';
|
||||
|
||||
class MetadataPluginAudioSourceEndpoint {
|
||||
final Hetu hetu;
|
||||
MetadataPluginAudioSourceEndpoint(this.hetu);
|
||||
|
||||
HTInstance get hetuMetadataAudioSource =>
|
||||
(hetu.fetch("metadataPlugin") as HTInstance).memberGet("audioSource")
|
||||
as HTInstance;
|
||||
|
||||
List<SpotubeAudioSourceContainerPreset> get supportedPresets {
|
||||
final raw = hetuMetadataAudioSource.memberGet("supportedPresets") as List;
|
||||
|
||||
return raw
|
||||
.map((e) => SpotubeAudioSourceContainerPreset.fromJson(e))
|
||||
.toList();
|
||||
}
|
||||
|
||||
Future<List<SpotubeAudioSourceMatchObject>> matches(
|
||||
SpotubeFullTrackObject track,
|
||||
) async {
|
||||
final raw = await hetuMetadataAudioSource
|
||||
.invoke("matches", positionalArgs: [track]) as List;
|
||||
|
||||
return raw.map((e) => SpotubeAudioSourceMatchObject.fromJson(e)).toList();
|
||||
}
|
||||
|
||||
Future<List<SpotubeAudioSourceStreamObject>> streams(
|
||||
SpotubeAudioSourceMatchObject match,
|
||||
) async {
|
||||
final raw = await hetuMetadataAudioSource
|
||||
.invoke("streams", positionalArgs: [match]) as List;
|
||||
|
||||
return raw.map((e) => SpotubeAudioSourceStreamObject.fromJson(e)).toList();
|
||||
}
|
||||
}
|
||||
@ -3,7 +3,9 @@ import 'dart:typed_data';
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:hetu_otp_util/hetu_otp_util.dart';
|
||||
import 'package:hetu_script/hetu_script.dart';
|
||||
import 'package:hetu_spotube_plugin/hetu_spotube_plugin.dart';
|
||||
import 'package:hetu_spotube_plugin/hetu_spotube_plugin.dart' as spotube_plugin;
|
||||
import 'package:hetu_spotube_plugin/hetu_spotube_plugin.dart'
|
||||
hide YouTubeEngine;
|
||||
import 'package:hetu_std/hetu_std.dart';
|
||||
import 'package:pub_semver/pub_semver.dart';
|
||||
import 'package:shadcn_flutter/shadcn_flutter.dart';
|
||||
@ -15,6 +17,7 @@ import 'package:spotube/models/metadata/metadata.dart';
|
||||
import 'package:spotube/services/metadata/apis/localstorage.dart';
|
||||
import 'package:spotube/services/metadata/endpoints/album.dart';
|
||||
import 'package:spotube/services/metadata/endpoints/artist.dart';
|
||||
import 'package:spotube/services/metadata/endpoints/audio_source.dart';
|
||||
import 'package:spotube/services/metadata/endpoints/auth.dart';
|
||||
import 'package:spotube/services/metadata/endpoints/browse.dart';
|
||||
import 'package:spotube/services/metadata/endpoints/playlist.dart';
|
||||
@ -22,13 +25,15 @@ import 'package:spotube/services/metadata/endpoints/search.dart';
|
||||
import 'package:spotube/services/metadata/endpoints/track.dart';
|
||||
import 'package:spotube/services/metadata/endpoints/core.dart';
|
||||
import 'package:spotube/services/metadata/endpoints/user.dart';
|
||||
import 'package:spotube/services/youtube_engine/youtube_engine.dart';
|
||||
|
||||
const defaultMetadataLimit = "20";
|
||||
|
||||
class MetadataPlugin {
|
||||
static final pluginApiVersion = Version.parse("1.0.0");
|
||||
static final pluginApiVersion = Version.parse("2.0.0");
|
||||
|
||||
static Future<MetadataPlugin> create(
|
||||
YouTubeEngine youtubeEngine,
|
||||
PluginConfiguration config,
|
||||
Uint8List byteCode,
|
||||
) async {
|
||||
@ -76,6 +81,58 @@ class MetadataPlugin {
|
||||
),
|
||||
);
|
||||
},
|
||||
createYoutubeEngine: () {
|
||||
return spotube_plugin.YouTubeEngine(
|
||||
search: (query) async {
|
||||
final result = await youtubeEngine.searchVideos(query);
|
||||
return result
|
||||
.map((video) => {
|
||||
'id': video.id.value,
|
||||
'title': video.title,
|
||||
'author': video.author,
|
||||
'duration': video.duration?.inSeconds,
|
||||
'description': video.description,
|
||||
'uploadDate': video.uploadDate?.toIso8601String(),
|
||||
'viewCount': video.engagement.viewCount,
|
||||
'likeCount': video.engagement.likeCount,
|
||||
'isLive': video.isLive,
|
||||
})
|
||||
.toList();
|
||||
},
|
||||
getVideo: (videoId) async {
|
||||
final video = await youtubeEngine.getVideo(videoId);
|
||||
return {
|
||||
'id': video.id.value,
|
||||
'title': video.title,
|
||||
'author': video.author,
|
||||
'duration': video.duration?.inSeconds,
|
||||
'description': video.description,
|
||||
'uploadDate': video.uploadDate?.toIso8601String(),
|
||||
'viewCount': video.engagement.viewCount,
|
||||
'likeCount': video.engagement.likeCount,
|
||||
'isLive': video.isLive,
|
||||
};
|
||||
},
|
||||
streamManifest: (videoId) {
|
||||
return youtubeEngine.getStreamManifest(videoId).then(
|
||||
(manifest) {
|
||||
final streams = manifest.audioOnly
|
||||
.map(
|
||||
(stream) => {
|
||||
'url': stream.url.toString(),
|
||||
'quality': stream.qualityLabel,
|
||||
'bitrate': stream.bitrate.bitsPerSecond,
|
||||
'container': stream.container.name,
|
||||
'videoId': stream.videoId,
|
||||
},
|
||||
)
|
||||
.toList();
|
||||
return streams;
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
await HetuStdLoader.loadBytecodeFlutter(hetu);
|
||||
@ -98,6 +155,7 @@ class MetadataPlugin {
|
||||
|
||||
late final MetadataAuthEndpoint auth;
|
||||
|
||||
late final MetadataPluginAudioSourceEndpoint audioSource;
|
||||
late final MetadataPluginAlbumEndpoint album;
|
||||
late final MetadataPluginArtistEndpoint artist;
|
||||
late final MetadataPluginBrowseEndpoint browse;
|
||||
@ -110,6 +168,7 @@ class MetadataPlugin {
|
||||
MetadataPlugin._(this.hetu) {
|
||||
auth = MetadataAuthEndpoint(hetu);
|
||||
|
||||
audioSource = MetadataPluginAudioSourceEndpoint(hetu);
|
||||
artist = MetadataPluginArtistEndpoint(hetu);
|
||||
album = MetadataPluginAlbumEndpoint(hetu);
|
||||
browse = MetadataPluginBrowseEndpoint(hetu);
|
||||
|
||||
13
pubspec.lock
13
pubspec.lock
@ -1230,10 +1230,10 @@ packages:
|
||||
description:
|
||||
path: "."
|
||||
ref: main
|
||||
resolved-ref: "01935a75640092af7947bfb21a497240376f0c83"
|
||||
resolved-ref: "32828156bc111d147709f8d644804227bbdfe8f1"
|
||||
url: "https://github.com/KRTirtho/hetu_spotube_plugin.git"
|
||||
source: git
|
||||
version: "0.0.1"
|
||||
version: "0.0.2"
|
||||
hetu_std:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -2888,10 +2888,11 @@ packages:
|
||||
youtube_explode_dart:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: youtube_explode_dart
|
||||
sha256: "9ff345caf8351c59eb1b7560837f761e08d2beaea3b4187637942715a31a6f58"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
path: "."
|
||||
ref: HEAD
|
||||
resolved-ref: caa3023386dbc10e69c99f49f491148094874671
|
||||
url: "https://github.com/Coronon/youtube_explode_dart"
|
||||
source: git
|
||||
version: "2.5.2"
|
||||
yt_dlp_dart:
|
||||
dependency: "direct main"
|
||||
|
||||
@ -138,7 +138,8 @@ dependencies:
|
||||
wikipedia_api: ^0.1.0
|
||||
win32_registry: ^1.1.5
|
||||
window_manager: ^0.4.3
|
||||
youtube_explode_dart: ^2.5.1
|
||||
youtube_explode_dart:
|
||||
git: https://github.com/Coronon/youtube_explode_dart
|
||||
yt_dlp_dart:
|
||||
git:
|
||||
url: https://github.com/KRTirtho/yt_dlp_dart.git
|
||||
|
||||
Loading…
Reference in New Issue
Block a user