Skip to content

Commit

Permalink
feat: local video library & playback
Browse files Browse the repository at this point in the history
this also includes:
- refactor for track duration to use as milliseconds not seconds
- refactor for many indexing functions
- rewrite of latest queue & other similar parts logic
- use global `lastPlayedIndex`
- improvements for home page loading
- faster loading for latest queue (by disabling maximumItems check)
- fix most played chips not being properly rebuilt
- fix default color tiles not enabled/disabled properly
- fix downloaded file not being added to library properly

* this feature is still under testing

ref: #115
  • Loading branch information
MSOB7YY committed Aug 24, 2024
1 parent 2acd505 commit dfaf5f5
Show file tree
Hide file tree
Showing 64 changed files with 1,384 additions and 869 deletions.
2 changes: 2 additions & 0 deletions android/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -96,11 +96,13 @@
<data android:scheme="content" />
<data android:scheme="file" />
<data android:mimeType="audio/*" />
<data android:mimeType="video/*" />
</intent-filter>
<intent-filter android:autoVerify="true">
<action android:name="android.intent.action.SEND_MULTIPLE" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="audio/*" />
<data android:mimeType="video/*" />
</intent-filter>
<intent-filter android:autoVerify="true">
<action android:name="android.intent.action.SEND" />
Expand Down
30 changes: 19 additions & 11 deletions android/app/src/main/kotlin/com/example/namida/FAudioTagger.kt
Original file line number Diff line number Diff line change
Expand Up @@ -149,16 +149,24 @@ public class FAudioTagger : FlutterPlugin, MethodCallHandler {
// waiting for confirmation before posting to stream
streamCompleters.get(streamKey)!!.get()
for (p in paths) {
val map =
readAllData(
p,
artworkDirectory,
artworkIdentifiers,
extractArtwork,
overrideArtwork,
)
map["path"] = p
withContext(Dispatchers.Main) { eventChannel.success(map) }
try {
withTimeout(6000) {
val map =
readAllData(
p,
artworkDirectory,
artworkIdentifiers,
extractArtwork,
overrideArtwork,
)
map["path"] = p
withContext(Dispatchers.Main) { eventChannel.success(map) }
}
} catch (_: Exception) {
val map = HashMap<String, Any>()
map["ERROR_FAULTY"] = true
withContext(Dispatchers.Main) { eventChannel.success(map) }
}
}
withContext(Dispatchers.Main) { eventChannel.endOfStream() }
_removeLogsUser()
Expand Down Expand Up @@ -235,7 +243,7 @@ public class FAudioTagger : FlutterPlugin, MethodCallHandler {
metadata["bitRate"] = audioHeader.getBitRateAsNumber()
metadata["sampleRate"] = audioHeader.getSampleRateAsNumber()
metadata["format"] = audioHeader.getFormat()
metadata["length"] = audioHeader.getTrackLength()
metadata["durationMS"] = Math.round(audioHeader.getPreciseTrackLength() * 1000)
}
} catch (e: Exception) {
writeError(path, "readAllData", "ERROR_HEADER", e.toString())
Expand Down
45 changes: 26 additions & 19 deletions lib/base/audio_handler.dart
Original file line number Diff line number Diff line change
Expand Up @@ -123,24 +123,25 @@ class NamidaAudioVideoHandler<Q extends Playable> extends BasicAudioHandler<Q> {

Future<void> _updateTrackLastPosition(Track track, int lastPositionMS) async {
/// Saves a starting position in case the remaining was less than 30 seconds.
final remaining = (track.duration * 1000) - lastPositionMS;
final remaining = track.durationMS - lastPositionMS;
final positionToSave = remaining <= 30000 ? 0 : lastPositionMS;

await Indexer.inst.updateTrackStats(track, lastPositionInMs: positionToSave);
}

@override
Future<void> tryRestoringLastPosition(Q item) async {
FutureOr<void> tryRestoringLastPosition(Q item) {
if (item is Selectable) {
final minValueInSet = settings.player.minTrackDurationToRestoreLastPosInMinutes.value * 60;
final minValueInSetMinutes = settings.player.minTrackDurationToRestoreLastPosInMinutes.value;

if (minValueInSet >= 0) {
if (minValueInSetMinutes >= 0) {
final minValueInSetMS = minValueInSetMinutes * 60 * 1000;
final seekValueInMS = settings.player.seekDurationInSeconds.value * 1000;
final track = item.track.toTrackExt();
final lastPos = track.stats.lastPositionInMs;
final lastPos = track.stats?.lastPositionInMs;
// -- only seek if not at the start of track.
if (lastPos >= seekValueInMS && track.duration >= minValueInSet) {
await seek(lastPos.milliseconds);
if (lastPos != null && lastPos >= seekValueInMS && track.durationMS >= minValueInSetMS) {
return seek(lastPos.milliseconds);
}
}
}
Expand Down Expand Up @@ -219,13 +220,12 @@ class NamidaAudioVideoHandler<Q extends Playable> extends BasicAudioHandler<Q> {
@override
void onIndexChanged(int newIndex, Q newItem) {
refreshNotification(newItem);
settings.player.save(lastPlayedIndex: newIndex);
newItem._execute(
selectable: (finalItem) {
settings.player.save(lastPlayedIndices: {LibraryCategory.localTracks: newIndex});
CurrentColor.inst.updatePlayerColorFromTrack(finalItem, newIndex);
},
youtubeID: (finalItem) {
settings.player.save(lastPlayedIndices: {LibraryCategory.youtube: newIndex});
CurrentColor.inst.updatePlayerColorFromYoutubeID(finalItem);
},
);
Expand Down Expand Up @@ -359,9 +359,9 @@ class NamidaAudioVideoHandler<Q extends Playable> extends BasicAudioHandler<Q> {
FutureOr<int> itemToDurationInSeconds(Q item) async {
return (await item._execute(
selectable: (finalItem) async {
final dur = finalItem.track.duration;
final dur = finalItem.track.durationMS;
if (dur > 0) {
return dur;
return dur ~/ 1000;
} else {
final ap = AudioPlayer();
final d = await ap.setFilePath(finalItem.track.path);
Expand Down Expand Up @@ -448,17 +448,18 @@ class NamidaAudioVideoHandler<Q extends Playable> extends BasicAudioHandler<Q> {
Future<void> onItemPlaySelectable(Q pi, Selectable item, int index, bool Function() startPlaying, Function skipItem) async {
final tr = item.track;
videoPlayerInfo.value = null;
final isVideo = item is Video;
Lyrics.inst.resetLyrics();
WaveformController.inst.resetWaveform();
WaveformController.inst.generateWaveform(
path: tr.path,
duration: Duration(seconds: tr.duration),
duration: Duration(milliseconds: tr.durationMS),
stillPlaying: (path) {
final current = currentItem.value;
return current is Selectable && path == current.track.path;
},
);
final initialVideo = await VideoController.inst.updateCurrentVideo(tr, returnEarly: true);
final initialVideo = await VideoController.inst.updateCurrentVideo(tr, returnEarly: true, handleVideoPlayback: false);

// -- generating artwork in case it wasnt, to be displayed in notification
File(tr.pathToImage).exists().then((exists) {
Expand All @@ -480,10 +481,16 @@ class NamidaAudioVideoHandler<Q extends Playable> extends BasicAudioHandler<Q> {
Future<Duration?> setPls() async {
if (!File(tr.path).existsSync()) throw PathNotFoundException(tr.path, const OSError(), 'Track file not found or couldn\'t be accessed.');
final videoOptions = initialVideo == null
? null
? isVideo
? VideoSourceOptions(
source: AudioVideoSource.file(item.path),
loop: false,
videoOnly: true,
)
: null
: VideoSourceOptions(
source: AudioVideoSource.file(initialVideo.path),
loop: VideoController.inst.canLoopVideo(initialVideo, duration?.inSeconds ?? tr.duration),
loop: VideoController.inst.canLoopVideo(initialVideo, duration?.inMilliseconds ?? tr.durationMS),
videoOnly: false,
);
final dur = await setSource(
Expand All @@ -494,7 +501,7 @@ class NamidaAudioVideoHandler<Q extends Playable> extends BasicAudioHandler<Q> {
isVideoFile: true,
);

Indexer.inst.updateTrackDuration(tr, dur);
if (dur != null) Indexer.inst.updateTrackDuration(tr, dur);

refreshNotification(currentItem.value);
return dur;
Expand Down Expand Up @@ -556,7 +563,7 @@ class NamidaAudioVideoHandler<Q extends Playable> extends BasicAudioHandler<Q> {

if (checkInterrupted()) return;

if (initialVideo == null) VideoController.inst.updateCurrentVideo(tr, returnEarly: false);
if (initialVideo == null) VideoController.inst.updateCurrentVideo(tr, returnEarly: false, handleVideoPlayback: false);

// -- to fix a bug where [headset buttons/android next gesture] sometimes don't get detected.
if (startPlaying()) onPlayRaw();
Expand Down Expand Up @@ -1822,7 +1829,7 @@ class NamidaAudioVideoHandler<Q extends Playable> extends BasicAudioHandler<Q> {

// ------- video -------

Future<void> setVideoSource({required AudioVideoSource source, bool loopingAnimation = false, bool isFile = false}) async {
Future<void> setVideoSource({required AudioVideoSource source, bool loopingAnimation = false, bool isFile = false, bool videoOnly = false}) async {
if (isFile && source is UriSource) File.fromUri(source.uri).setLastAccessedTry(DateTime.now());
final videoOptions = VideoSourceOptions(
source: source,
Expand Down Expand Up @@ -1893,7 +1900,7 @@ extension TrackToAudioSourceMediaItem on Selectable {
artist: artist,
album: tr.hasUnknownAlbum ? '' : tr.album,
genre: tr.originalGenre,
duration: duration ?? Duration(seconds: tr.duration),
duration: duration ?? Duration(milliseconds: tr.durationMS),
artUri: Uri.file(File(imagePage).existsSync() ? imagePage : AppPaths.NAMIDA_LOGO_MONET),
);
}
Expand Down
4 changes: 2 additions & 2 deletions lib/base/generator_base.dart
Original file line number Diff line number Diff line change
Expand Up @@ -38,15 +38,15 @@ abstract class NamidaGeneratorBase<T extends ItemWithDate, E> {
return randomListMap.keys;
}

Iterable<E> generateRecommendedItemsFor(E item, E Function(T current) itemToSub) {
Iterable<E2> generateRecommendedItemsFor<E2>(E item, E2 Function(T current) itemToSub) {
final historytracks = historyController.historyTracks.toList();
if (historytracks.isEmpty) return [];

const length = 10;
final max = historytracks.length;
int clamped(int range) => range.clamp(0, max);

final Map<E, int> numberOfListensMap = {};
final Map<E2, int> numberOfListensMap = {};

for (int i = 0; i <= historytracks.length - 1;) {
final t = historytracks[i];
Expand Down
2 changes: 1 addition & 1 deletion lib/base/pull_to_refresh.dart
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,7 @@ mixin PullToRefreshMixin<T extends StatefulWidget> on State<T> implements Ticker
_isRefreshing = false;
onVerticalDragFinish();
} catch (_) {
await _animation2.fling();
if (mounted) await _animation2.fling();
if (mounted) _animation2.stop();
_isRefreshing = false;
onVerticalDragFinish();
Expand Down
2 changes: 1 addition & 1 deletion lib/class/color_m.dart
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ class NamidaColor {
return NamidaColor(
used: json['used'] != null ? Color(json['used']) : null,
mix: Color(json['mix'] ?? 0),
palette: List<Color>.from(List<int>.from(json['palette'] ?? []).map((e) => Color(e))),
palette: (json['palette'] as List?)?.map((e) => Color(e as int)).toList() ?? [],
);
}

Expand Down
8 changes: 4 additions & 4 deletions lib/class/faudiomodel.dart
Original file line number Diff line number Diff line change
Expand Up @@ -196,7 +196,7 @@ class FTags {

class FAudioModel {
final FTags tags;
final int? length;
final int? durationMS;
final int? bitRate;
final String? channels;
final String? encodingType;
Expand All @@ -209,7 +209,7 @@ class FAudioModel {

const FAudioModel({
required this.tags,
this.length,
this.durationMS,
this.bitRate,
this.channels,
this.encodingType,
Expand All @@ -228,7 +228,7 @@ class FAudioModel {
factory FAudioModel.fromMap(Map<String, dynamic> map) {
return FAudioModel(
tags: FTags.fromMap(map),
length: map["length"],
durationMS: map["durationMS"],
bitRate: map["bitRate"],
channels: map["channels"],
encodingType: map["encodingType"],
Expand All @@ -244,7 +244,7 @@ class FAudioModel {
Map<String, dynamic> _toMapMini() {
final tagsMap = tags.toMap();
tagsMap.addAll(<String, dynamic>{
"length": length,
"durationMS": durationMS,
"bitRate": bitRate,
"channels": channels,
"encodingType": encodingType,
Expand Down
59 changes: 43 additions & 16 deletions lib/class/folder.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,36 @@
import 'dart:io';

import 'package:namida/class/track.dart';
import 'package:namida/controller/folders_controller.dart';
import 'package:namida/controller/indexer_controller.dart';
import 'package:namida/core/extensions.dart';

final _pathSeparator = Platform.pathSeparator;

class VideoFolder extends Folder {
VideoFolder.explicit(super.path) : super.explicit();

@override
String toString() => "VideoFolder(path: $path, tracks: ${tracks().length})";
}

class Folder {
final String path;
late final String folderName;
late final String _key;

Folder(this.path)
Folder.explicit(this.path)
: folderName = path.pathReverseSplitter(_pathSeparator),
_key = _computeKey(path);

static T fromType<T extends Folder>(String path) {
return T == VideoFolder ? VideoFolder.explicit(path) as T : Folder.explicit(path) as T;
}

static T fromTypeParameter<T extends Folder>(Type type, String path) {
return type == VideoFolder ? VideoFolder.explicit(path) as T : Folder.explicit(path) as T;
}

static String _computeKey(String path) {
final addAtFirst = !path.startsWith(_pathSeparator);
if (!path.endsWith(_pathSeparator)) path += _pathSeparator;
Expand Down Expand Up @@ -58,18 +74,30 @@ class Folder {
String toString() => "Folder(path: $path, tracks: ${tracks().length})";
}

extension FolderUtils on Folder {
Folder get parent {
extension FolderUtils<T extends Folder, E extends Track> on Folder {
Map<T, List<E>> get _mainFoldersMap {
return this is VideoFolder ? Indexer.inst.mainMapFoldersVideos.value as Map<T, List<E>> : Indexer.inst.mainMapFolders.value as Map<T, List<E>>;
}

Folders get _controller {
return this is VideoFolder ? Folders.videos : Folders.tracks;
}

void navigate() {
_controller.stepIn(this);
}

T get parent {
final parentPath = FileSystemEntity.parentOf(path);
return Folder(parentPath);
return Folder.fromTypeParameter(this.runtimeType, parentPath) as T;
}

/// Checks if any other folders inside library have the same name.
///
/// Can be heplful to display full path in such case.
bool get hasSimilarFolderNames {
int count = 0;
for (final k in Indexer.inst.mainMapFolders.value.keys) {
for (final k in _mainFoldersMap.keys) {
if (k.folderName == folderName) {
count++;
if (count > 1) return true;
Expand All @@ -78,39 +106,38 @@ extension FolderUtils on Folder {
return false;
}

List<Track> tracks() => Indexer.inst.mainMapFolders.value[this] ?? [];
List<E> tracks() => _mainFoldersMap[this] ?? [];

Iterable<Track> tracksRecusive() sync* {
for (final e in Indexer.inst.mainMapFolders.value.entries) {
Iterable<E> tracksRecusive() sync* {
for (final e in _mainFoldersMap.entries) {
if (this.isParentOf(e.key)) {
yield* e.value;
}
}
}

/// checks for the first parent folder that exists in [Indexer.mainMapFolders].
Folder? getParentFolder() {
T? getParentFolder() {
final parts = path.split(_pathSeparator);
parts.removeLast();

while (parts.isNotEmpty) {
final f = Folder(parts.join(_pathSeparator));
if (Indexer.inst.mainMapFolders.value[f] != null) return f;
final f = Folder.fromTypeParameter(this.runtimeType, parts.join(_pathSeparator));
if (_mainFoldersMap[f] != null) return f as T;
parts.removeLast();
}

return null;
}

/// Gets directories inside [this] folder, automatically handles nested folders.
List<Folder> getDirectoriesInside() {
final foldersMap = Indexer.inst.mainMapFolders;
final allInside = <Folder>[];
List<F> getDirectoriesInside<F extends Folder>() {
final allInside = <F>[];

final splitsCount = this.splitParts().length;

for (final folder in foldersMap.value.keys) {
if (this.isDirectParentOf(folder, splitsCount)) allInside.add(folder);
for (final folder in _mainFoldersMap.keys) {
if (this.isDirectParentOf(folder, splitsCount)) allInside.add(folder as F);
}

return allInside;
Expand Down
Loading

0 comments on commit dfaf5f5

Please sign in to comment.