Skip to content

ubuntu-flutter-community/musicpod

Repository files navigation

MusicPod

Translation status

Dart Flutter

MusicPod is a local music, radio, television and podcast player for Linux Desktop, MacOS and Windows (currently the metadata loading for local audio takes ages, so the Windows version is not considered stable if you are looking for the local audio feature. See #264 ). (Android is planed but no ETA yet when it will happen.)

OS How to install
Linux Get it from the Snap Store
or

or (Arch Linux)
install from archlinuxcn via sudo pacman -S musicpod
Windows Release Page
MacOS Release Page
Android WIP

Features

Features Dark Linux Light Linux Dark MacOS Light MacOS
Play local audio
Find local audios sorted by Metadata
Play radio stations, with icytags and artwork looked up!
Play and download podcasts, safe progress, sort episodes and more!
Video podcast support!
Discover podcasts, filtered as you like
Discover radio stations, filtered as you like
Different view modes

Credits

AppIcon

The app icon has been made by Stuart Jaggers, thank you very much Stuart!

Flatpak

Thanks TheShadowOfHassen for packaging MusicPod as a Flatpak!

Libraries used

Thanks to all the MPV contributors!

Thank you @amugofjava for creating the very easy to use and reliable podcast_search!

Thanks @alexmercerind for the super performant Mediakit library and mpris_service dart implementation!

Thank you @KRTirtho for the very easy to use smtc_windows package and Flutter Discord RPC

Thank you @tomassasovsky for the dart implementation of radiobrowser-api!

Thank you @ClementBeal for the super fast, pure dart Audio Metadata Reader!

Thank you @escamoteur for creating get_it, watch_it, command_it and listen_it, which made my application faster and the source code cleaner!

Contributing

Contributions are highly welcome. Especially translations.

Translations

The help of translators is highly appreciated! For translations MusiPod is now on Weblate thanks to @keunes:

https://hosted.weblate.org/projects/musicpod/app

Translation status

Code contributions

If you find any error please feel free to report it as an issue and describe it as good as you can. If you want to contribute code, please create an issue first, then proceed as follows:

Please fork MusicPod to your GitHub namespace, clone it to your computer, create a branch named by yourself, commit your changes to your local branch, push them to your fork and then make a pull request from your fork to this repository. I recommend the vscode extension GitHub Pull Requests especially for people new to Git and GitHub.

Build

Prerequisites

  • for Linux desktop builds: sudo apt install libglib2.0-dev libgtk-3-dev curl git unzip xz-utils zip libglu1-mesa libmpv-dev mpv libnotify-dev
  • for macos, windows and android please check the official instructions for your target platform!
  • install flutter
    • I suggest to install the flutter sdk with FVM (this does not replace the native dependencies ofc!)
  • required for android builds: install android-studio
  • required for macos builds: install xcode
  • as a good IDE for all builds: install vcode

Generate some code

The dependencies and test mocks are generated with build_runner. You need to run the build_runner command in order to re-generate dependencies, in case you changed the signatures of service methods, like this:

dart pub run build_runner build --force-jit

or if you use fvm:

fvm dart pub run build_runner build --force-jit

Run

Now you can run the app with flutter run or fvm flutter run in the project directory. If not, please open an issue if I maybe have forgotten a step which I had locally, but not in the README.md.

Release

The release please bot automatically creates a pull request when new changes after the last release are made with conventional commits. It force pushes those to its branch and updates the version string in pubspec.yaml and the changelog file. When the release please pull request is merged the version and changelog will be updated in main.

Linux

The linux snap packages are always build when a new commit lands in main. When a new release is made go to https://snapcraft.io/musicpod/releases and promote the latest snap from edge to stable.

MacOs

  • fvm flutter build macos --release
  • open the macos directory in xcode
    • in xcode go to "Product" -> "Archive"
    • create archive
    • validate
    • distribute
    • notarize
    • export app
  • install create-dmg
  • open the location you exported the app to
  • create-dmg --idenfity=XXXXXXXX musicpod.app (XXXXXXXX is your apple dev ID which needs to be with your certificate in your mac...)

Windows

Currently inno setup is needed. This hopefully changes some day when #964 is merged.

  • fvm flutter build windows --release
  • copy the following files from C:\Program Files (x86)\Microsoft Visual Studio\2022\Community\VC
    Redist\MSVC\14.34.31931\x64\Microsoft.VC143.CRT\ into your windows build
msvcp140.dll
msvcp140_1.dll
msvcp140_2.dll
vcruntime140.dll
vcruntime140_1.dll
  • run .../musicpod/windows/inno.iss

Testing

Test mocks are generated with Mockito. You need to run the build_runner command in order to re-generate mocks, in case you changed the signatures of service methods.

dart run build_runner build

Boring developer things

Under the flutter hood

MusicPod is basically a fancy front-end for MPV! Without it it would still look nice, but it wouldn't play any media :D!

State

The ever living discussion about state (Initially I didn't really like the word "state", in german it is "Zustand") in flutter is certainly an amusing topic. Often the word "state" means simply "data", "loading" and "error" state. Flutter let's the developer choose how to manage state, which can be awkward coming from UI frameworks with inbuilt data binding and state management solutions.

Amusing or not, it is incredibly important to understand where you keep state alive and when you want to dispose it and to treat the memory of the compuers of your users with respect!

So the "state management" topic is at least three fold:

  • life cycle: when do we create the state and when do we dispose it?
  • access and data binding: how do we access the state, how do we update it and how do we bind the state/data to the UI?
  • async operations and their states: how do we handle loading, error and data "sub"-states for async operations?

Flutter provides very good out of the box solutions with State, StatefulWidgets and InheritedWidgets. Also for async state we have FutureBuilder and StreamBuilder.

But managing state becomes harder if your data is not only async but also needs to be shared across multiple widgets and you want to update and listen to it from different places in the app. And in case of MusicPod that data consists of local music, podcasts and radio stations, which all have different sources (and in case of slocal audio files, I initially underestimated how big the library of some users are...) which means you deal with huge lists of objects, which means huge amount of data, which means huge amount of allocated memory.

So if you want to access state from different places in your app, one could just make it available everywhere inside the app. But this means that the instantiated objects living in the heap of your apps process never will be garbage collected, which is really bad for every app.

So I was looking for a solution that:

  • makes it easy to access state wherever I want, preferably without needing to manage flutter's build context
  • making it easy to use async state and update the UI accordingly to loading and error states
  • cut the state "potions" as small as possible
  • dispose the state whenever it is not needed anymore, so that the memory can be freed

Why not just use stateful widgets then?

Stateful widgets work fine and exactly as they should and dispose the data instantiated within them when the widgets are disposed but as soon as the state consists of multiple objects or fields it becomes hard to only redraw the parts that should be redrawn. And also when the the state needs to be created and updated asynchronoulsy it becomes a pain to only update loading error and loading states for this particular piece of state that you are currently interested in. Long story short: at a certain feature amount and size of your app, stateful widgets become a pain to manage and you need to look for a more scalable solution.

Resulting package choices

So I found my personal favorite solution with get_it plus watch_it plus command_it and listen_it. Additionally, to make my life easier when connecting the dependencies of classes I use Injectable for code generation of the dependency graph of get_it, by annotating the constructors of my classes and then running the build_runner command to generate the code for get_it.

(get_it, watch_it, listen_it and command_it be obtained with flutter_it which is a meta package for all of them.)

Architecture

  • UI / View
    • with widgets builds methods cut as small as possible, to keep the build methods fast and to not lose the overview of the UI code
  • Manager classes, updating the state and thus the UI
    • they provide "commands", which are async/sync operations which update their data, loading and error state
    • key to memory succes is here using get_it's cached factories!, which's data is garbagge collected when not used anymore!
  • Service classes, defining the actualy operations in form of methods of a service class
  • DAO-classes, as an abstration layer for the database
  • a database, which is currently implemented with drift and sqlite, to persist data, and make it available across app restarts

Examples

WIP

Performance

Reading the local covers and fetching remote covers for radio data happens inside additional second dart isolates. When idle MusiPod's CPU power consumption is 0%. For a 10 years old intel dual core, the CPU usage is about 2% while playing music, since only the parts are redrawn which need to be, thanks to watch_it. Memory allocation can spike depending on the size of a page's data, but it is freed as soon as the user leaves the page, thanks to get_it's cached factories (the same behaviour can ofc be achieved with flutter's stateful widgets, but I prefer the get_it solution for the reasons mentioned above in the "state" section).

Persistence

Preferences are stored with shared_preferences, the rest is stored within drift in a local sqlite database.

About

Music, radio, television and podcast player for Ubuntu, MacOs and maybe soon Android

Topics

Resources

License

Stars

642 stars

Watchers

8 watching

Forks

Sponsor this project

 

Contributors