flypig.co.uk

List items

Items from the current list are shown below.

Newpipe

All items from March 2025

13 Mar 2025 : Day 4 #
Yesterday we built the NewPipe Extractor for desktop Linux. Our ultimate aim is to get it built for use with Sailfish OS so that the API can be called from C++ or QML code.

But we'll need to spend a bit of time building up to that, because I want to make use of a very specific approach to using Java code on Sailfish OS.

This was something I learnt several years ago from the esteemed Java and Sailfish OS crossover expert Thigg. Back in 2022 I wrote a piece for the Sailfish Community News on a project called Sailing-to-Coffee that Thigg created. Thigg built a pipeline for making use of GraalVM with Sailfish OS.

At the time I thought it was shear brilliance. As time has gone on my opinion hasn't changed and so I'm keen to make use of the technique with NewPipe as well.

I'll let you read the article from the Community News to learn about what's going on. What I wrote there is still valid today. Today, however, I'm going to download the code for Thigg's sailing-the-flood-to-java application, which neatly demonstrates the technique.

If you've been following along closely you'll also note that this approach was also more recently suggested by pherjung, having read about it on the Sailfish OS Forum.

I'm especially gratified that following that, Thigg also got in touch to offer to contribute. We've arranged to have a call to discuss this further and I'm expecting this to add rocket fuel to the project!

Getting the GraalVM pipeline set up is a bit of work, so it'll be helpful to get this successfully running before returning to the NewPipe Extractor code.

To do this I'm going to start by setting up Thigg's sailing-the-flood-to-java application, or at least the Java build part, so that it outputs a native library for use on Sailfish OS built from the Java code in the repository. I've got my own fork of the code and that's where I'm going to start:
git clone git@github.com:llewelld/sailing-the-flood-to-java.git
cd sailing-the-flood-to-java
git submodule update --init --recursive
cd java-part
Unfortunately some things have changed since Thigg set up this repository. In particular it seems Project Lombok now needs to be added as a dependency (maybe it always did, but at least I've found I can't now build it without). This can be added to the plugins section of the pom.xml file. All I've done is add the following inside the build/plugins element:
<plugin>
    <groupId>org.apache.maven.plugins</groupid>
    <artifactId>maven-compiler-plugin</artifactid>
    <version>3.6.1</version>
    <configuration>
        <annotationProcessorPaths>
            <path>
                <groupId>org.projectlombok</groupid>
                <artifactId>lombok</artifactid>
                <version>1.18.30</version>
            </path>
        </annotationprocessorpaths>
    </configuration>
</plugin>
I've also bumped up the Lombok version to 1.18.30:
         <dependency>
             <groupId>org.projectlombok</groupid>
             <artifactId>lombok</artifactid>
-            <version>1.18.24</version>
+            <version>1.18.30</version>
         </dependency>
For context, Lombok doesn't provide any user-facing functionality for the application, but it does make writing Java code easier. For example, rather than having to write getter and setter methods for each field you can just annotate a class with @Data. Similarly you can use @NoArgsConstructor to have Lombok create the constructor for you.

Here's some example code taken from the NewGameParams.java file that illustrates this:
@Data
@NoArgsConstructor
public class NewGameParams {
    public int size;
    public int numColors;
}
This is equivalent to having to write out the following code in full:
@Data
@NoArgsConstructor
public class NewGameParams {
    public int size;
    public int numColors;
    
    public int getSize() { return size; };
    public void setSize(int size) { this.size = size;};
    public int getNumColors() { return numColors; };
    public void setNumColors(int numColors) { this.numColors = numColors; }

    public GameStateT() {
        this.size = 0;
        this.numColors = 0;
    }
}
You can see why people like Lombok. But it does add an extra dependency, so it's inclusion isn't entirely pain free.

Apart from that we also need to update the GraalVM configuration. GraalVM is going to provide us with the real magic, so we need to get this right. In the original version of the pom.xml was the following snippet of XML:
    <dependency>
        <groupId>org.graalvm.sdk</groupid>
        <artifactId>graal-sdk</artifactid>
        <version>22.1.0.1</version>
    </dependency>
This sets up the GraalVM to be a build dependency. But it turns out GraalVM doesn't like this anymore, so I've completely removed it as a dependency. We do still need GraalVM of course, but it's already defined as a plugin for native builds in the same file and it turns out that's as much as we need now.

That sets up the build structure. The next step is to adjust the compile.sh script. This script is unusual for developing code on Sailfish OS because, rather than building things in the Sailfish SDK, it instead copies the code to an actual phone and builds it there.

This may sound a little crazy, but it also makes perfect sense. One of the key motivations for having the Sailfish SDK is that it mimics both the environment and the target architecture (the CPU type: x86, arm, aarch64, etc.) of the phone you want your code to run on.

Why not avoid all that by going directly to the phone and building it there?

Going direct to the phone has a number of benefits. Unless it happens to match the architecture of the system you're working on, the Sailfish SDK has to emulate the target architecture, which can be very slow. The SDK performs a number of clever tricks to try to speed things up, such as switching out emulated tools for native tools where it can, but it still has an impact. Going direct to the phone also relaxes some restrictions that might otherwise apply to the Java compiler, specifically around memory usage.

It's the memory usage restraint that's key for us here. I've tried to set GraalVM up for use within the Sailfish SDK in the past with only limited success. It's possible things have changed in the meantime, so with this project I'm planning to give this another go at some point.

But first things first, let's get a working build.

The next step is to set up GraalVM on a phone. Thankfully all that's needed for this is a directory to unpack the appropriate GraalVM JDK archive into.
$ ssh defaultuser@172.28.172.1
$ mkdir ~/Documents/Development/newpipe/graalvm
$ cd ~/Documents/Development/newpipe/graalvm
$ curl -O https://download.oracle.com/graalvm/23/latest/
    graalvm-jdk-23_linux-aarch64_bin.tar.gz
$ tar -xf graalvm-jdk-23_linux-aarch64_bin.tar.gz
I executed these commands on my phone. Note also that the root of the project folder we're using is /home/defaultuser/Documents/Development/newpipe/. We'll be needing this again in the near future.

The next step is to take a look at and edit the compile.sh shell script. We do this not on the phone but on the device (desktop, laptop,...) we're using to connect to our phone for development. In future I'll refer to this as the orchestrating device, because it's the device that's orchestrating the build.

Here's what I've got in the compile.sh file:
#!/bin/bash
COMPILEHOST=defaultuser@172.28.172.1
COMPILEHOST_WORKSPACE=/home/defaultuser/Documents/Development/newpipe/java-part
COMPILEHOST_GRAAL=/home/defaultuser/Documents/Development/newpipe/graalvm/
    graalvm-jdk-23.0.2+7.1
COMPILEHOST_MAVENLOCAL=/home/defaultuser/Documents/Development/newpipe/graalvm/
    m2
echo &quot;transfering data&quot;
rsync . $COMPILEHOST:$COMPILEHOST_WORKSPACE -r
echo &quot;starting compilation&quot;
ssh $COMPILEHOST &quot;cd $COMPILEHOST_WORKSPACE; 
    GRAALVM_HOME=$COMPILEHOST_GRAAL \
    JAVA_HOME=$COMPILEHOST_GRAAL ./mvnw \
    -Dmaven.repo.local=$COMPILEHOST_MAVENLOCAL clean install -Pnative&quot;
scp $COMPILEHOST:&quot;$COMPILEHOST_WORKSPACE/target/*.h&quot; ../qt-part/lib/
scp $COMPILEHOST:&quot;$COMPILEHOST_WORKSPACE/target/javafloodjava.so&quot; \
    ../qt-part/lib/libjavafloodjava.so
Let's go through this file step-by-step to explain what's going on and so — if you're following along — you can update it with values relevant to your set up.

First we set the COMPILEHOST environment variable. This should contain the endpoint string for logging in to your phone via SSH. Typically this includes a username and an IP address. As you can see, that's what I've got for my setup.

Second we set COMPILEHOST_WORKSPACE to the directory where we want the Java code to be copied over to. Earlier we created a directory /home/defaultuser/Documents/Development/newpipe/ and now we're using a subdirectory of this called java-part, because it's where the JAVA code is going to go.

Next we set the COMPILEHOST_GRAAL environment variable. This should point to the directory where we unpacked the GraalVM to. This contains all of the GraalVM code, as we can see:
$ ls /home/defaultuser/Documents/Development/newpipe/graalvm/
    graalvm-jdk-23.0.2+7.1
GRAALVM-README.md  conf     legal                                man
LICENSE.txt        include  lib                                  release
bin                jmods    license-information-user-manual.zip
The final environment variable we set up is COMPILEHOST_MAVENLOCAL. This is something I added myself because otherwise Maven (the build tool that we're going to use) will store all of the files it downloads to a hidden directory on the phone. I really dislike it when tools do this; it's problematic for many reasons, not least because there's a good chance we'll want to remove all these files later. I always like to know where things are getting stored.

Later on in this script there's a ./mvnw command that gets run on the phone and I've added the COMPILEHOST_MAVENLOCAL reference to this.

There's no need to change the rsync command. This copies the source directory over to the phone ready to be built there.

Although I've edited the proceeding SSH statement, this shouldn't need configuring any further.

At the end there's an scp command which is used to copy the resulting library file that's built off the phone and back on to the orchestrating device.

And that's it. All of the really hard work is going to be done by the mvnw command.

When we execute the script, it will SSH into the phone, build the library and then copy it back, like this:
$ ./compile.sh 
transfering data
starting compilation
[INFO] Scanning for projects...
[INFO] 
[INFO] -----------------------< de.thigg:javafloodjava >-----------------------
[INFO] Building javafloodjava 1.0-SNAPSHOT
[INFO] --------------------------------[ jar ]---------------------------------
[...]

-------------------------------------------------------
 T E S T S
-------------------------------------------------------
Running GameTest
Tests run: 2, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.169 sec
Running InterfaceTest
Tests run: 0, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.001 sec

Results :

Tests run: 2, Failures: 0, Errors: 0, Skipped: 0
[...]

===============================================================================
GraalVM Native Image: Generating 'javafloodjava' (shared library)...
===============================================================================
For detailed information and explanations on the build output, visit:
https://github.com/oracle/graal/blob/master/docs/reference-manual/native-image/
    BuildOutput.md
-------------------------------------------------------------------------------
[1/8] Initializing...                                          (21.0s @ 0.09GB)
[...]
[2/8] Performing analysis...  [*****]                          (87.7s @ 0.45GB)
    4,354 reachable types   (74.2% of    5,871 total)
    5,622 reachable fields  (48.6% of   11,558 total)
   24,439 reachable methods (50.4% of   48,535 total)
    1,343 types,    14 fields, and   159 methods registered for reflection
       57 types,    57 fields, and    52 methods registered for JNI access
        4 native libraries: dl, pthread, rt, z
[3/8] Building universe...                                     (11.1s @ 0.51GB)
[4/8] Parsing methods...      [*****]                          (26.9s @ 0.58GB)
[5/8] Inlining methods...     [***]                             (9.4s @ 0.66GB)
[6/8] Compiling methods...    [**************]                (220.0s @ 1.09GB)
[7/8] Laying out methods...   [****]                           (13.8s @ 1.25GB)
[8/8] Creating image...       [***]                            (11.4s @ 0.39GB)
[...]
-------------------------------------------------------------------------------
   26.6s (6.4% of total time) in 2369 GCs | Peak RSS: 1.83GB | CPU load: 5.45
-------------------------------------------------------------------------------
Build artifacts:
 newpipe/java-part/target/graal_isolate.h (c_header)
 newpipe/java-part/target/graal_isolate_dynamic.h (c_header)
 newpipe/java-part/target/javafloodjava.h (c_header)
 newpipe/java-part/target/javafloodjava.so (shared_library)
 newpipe/java-part/target/javafloodjava_dynamic.h (c_header)
===============================================================================
Finished generating 'javafloodjava' in 6m 52s.
[...]
graal_isolate.h                                   100% 5426   262.1KB/s   00:00
graal_isolate_dynamic.h                           100% 5538   804.7KB/s   00:00
javafloodjava.h                                   100%  422    74.9KB/s   00:00
javafloodjava_dynamic.h                           100%  478    64.9KB/s   00:00
javafloodjava.so
As you can see, we're left with a juicy-looking javafloodjava.so native library. That's what the C++ code will end up calling.

My plan is to have a similar arrangement to get NewPipe Extractor to build to a native library on my phone. But before doing that we should first also explore how Thigg managed to get the Java methods exposed for use in this library. That's what we'll do tomorrow.

I also want to try once again to get this building on the Sailfish SDK. This would have some benefits, for example it might allow us to build using Chum/OBS. That'll be for a future entry as well.

One final thing I wanted to mention is that yesterday I received a helpful email from Micha? Szczepaniak, author of microTube, a YouTube app for Sailfish OS.

Micha? suggested that rather than build an entirely new app, an alternative would be to set up NewPipe Extractor as a backend for microTube. He highlighted in particular that he's already had to switch out the backend before, given the projects he's used to access YouTube have had a tendency to come and go. Much like NewPipe, this has left microTube with a good separation between the front-end and backend code.

I personally think this would be a great idea and I'm grateful to Micha? for reaching out to me. As this NewPipe project goes on I think it will become clearer whether this is a good fit or not. For example, NewPipe supports multiple services, not just YouTube, so this may or may not fit well with the approach microTube uses. But I'm very much in favour of building up the existing ecosystem rather than fragmenting it, so this is an option I'll be taking a very serious look into.
12 Mar 2025 : Day 3 #
Yesterday we cloned the NewPipe Extractor repository and built ourselves the library. We weren't able to do anything with the resulting extractor-v0.24.5.jar archive, but our plan is to rectify that today.

The good news is that someone else has already come up with a nice wrapper for making use of the library, called New Valve. The bad news is that this is written in Kotlin and we really want to stick to Java.

So today I'll have to spend a little time converting it to Java. There's actually not a lot of code involved, so this shouldn't be too arduous.

The key piece of code is the DownloaderImpl class. This uses OkHttp to create a simple HTTP Client which is provided to the NewPipe Extractor class and used for downloading data over HTTP.

Presumably NewPipe Extractor doesn't come with its own HTTP Client because different apps will have different ways of wanting to provide this functionality. This OkHttp wrapper looks like it'll be more than enough for our needs.

To get this all setup I've created a new directory appwrapper inside the repository which contains three files and, as is typically for Java projects, way too much directory depth.
$ tree appwrapper/
appwrapper/
??? build.gradle
??? src
    ??? main
        ??? java
            ??? uk
                ??? co
                    ??? flypig
                        ??? DownloaderImpl.java
                        ??? Main.java

6 directories, 3 files
In case you're not familiar with Java, convention is that you put your file in a namespace that's the inverse of a URL. In my case I'm using uk.co.flypig. Unlike for C++ you can only have one class per file; the filename has to match the classname and the folder hierarchy typically matches the namespace.

Hence the deep and largely empty directory hierarchy.

As you may have guessed the build.gradle contains the instructions for building the code. This file is actually surprisingly terse:
plugins {
    id 'checkstyle'
    id 'application'
}

apply plugin : &quot;java&quot;

application {
    mainClass = 'uk.co.flypig.Main'
}

checkstyle {
    getConfigDirectory().set(rootProject.file(&quot;checkstyle&quot;))
    ignoreFailures = false
    showViolations = true
    toolVersion = checkstyleVersion
}

checkstyleTest {
    enabled = false // do not checkstyle test files
}

dependencies {
    implementation project(':timeago-parser')
    api project(':extractor')

    implementation &quot;com.squareup.okhttp3:okhttp:3.12.13&quot;
}
Most of this is — I think — self explanatory. The checkstyle sections are to allow the code style to be tested against the requirements for the project (so I could have left these sections out if I'd wanted). The dependencies section highlights that the code makes use of the two other projects in the repository, along with OkHttp. With these added gradle will deal with downloading and hooking in the dependencies automatically.

The only other thing to note is that the application section tells gradle how to execute the code.

I also had to make some small changes to the gradle configuration of the containing directory (the top level of the repository). First to add the appwrapper directory to build.gradle, like so:
diff --git a/build.gradle b/build.gradle
index 335960db..b57add8c 100644
--- a/build.gradle
+++ b/build.gradle
@@ -37,4 +37,5 @@ allprojects {
 dependencies {
     api project(':extractor')
     implementation project(':timeago-parser')
+    implementation project(':appwrapper')
 }
Second to add appwrapper to the settings.gradle like so:
diff --git a/settings.gradle b/settings.gradle
index 725f6505..5cdd3d7d 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -1,1 +1,1 @@
-include 'extractor', 'timeago-parser'
+include 'extractor', 'timeago-parser', 'appwrapper'
Let's now turn to the code itself. I'm not going to paste it all here, but I will link to it in the repository. First we have the DownloaderImpl class. This is a conversion of the Kotlin class of the same name that we saw earlier. It extends the Downloader interface from the Extractor by providing init() and execute() methods.

In short, the execute() method is called by the Extractor when it wants to download a file from somewhere. The method returns a Response object, also defined as part of the Extractor interface, which represents the data retrieved.

The OkHttp library uses a thread pool to parallelise HTTP requests, but it likes to keep this pool around in case there are any new connections to be made. In testing I found this caused the app to linger for a minute or more before quitting while the threadpool awaits deinitialise. I therefore also added a teardown() method which forcefully deactivates the threadpool. This gets called when the app quits so that it returns immediately, rather than after a prolonged delay.

This DownloaderImp class then gets used in our main() method, which is really just a test function so we know things are working correctly:
public static void main(final String[] args) {
    System.out.println(&quot;Initialising&quot;);
    final String url = args.length > 0
        ? args[0]
        : &quot;https://www.youtube.com/watch?v=xvFZjo5PgG0&quot;;
    final OkHttpClient.Builder builder = new OkHttpClient.Builder();
    final DownloaderImpl downloader = DownloaderImpl.init(builder);
    NewPipe.init(downloader);

    try {
        System.out.println(&quot;Downloading video&quot;);
        System.out.println(&quot;URL: &quot; + url);
        final StreamingService service = ServiceList.YouTube;
        final StreamExtractor extractor = service.getStreamExtractor(url);
        extractor.fetchPage();

        System.out.println(&quot;Video name: &quot; + extractor.getName());
        System.out.println(&quot;Uploader: &quot; + extractor.getUploaderName(
    ));
        System.out.println(&quot;Category: &quot; + extractor.getCategory());
        System.out.println(&quot;Likes: &quot; + extractor.getLikeCount());
        System.out.println(&quot;Views: &quot; + extractor.getViewCount());

        final java.util.List<VideoStream> streams = extractor.getVideoStreams();
        for (final VideoStream stream : streams) {
            System.out.println(&quot;Content: &quot; + stream.getContent());
        }

    } catch (final Exception e) {
        System.out.println(&quot;Exception: &quot; + e);
    }

    try {
        downloader.teardown();
    } catch (final IOException e) {
        System.out.println(&quot;IOException: &quot; + e);
    }

    System.out.println(&quot;Completed&quot;);
}
All this code does is initialise the NewPipe extractor by giving it a DownloaderImpl instance. It then calls getStreamExtractor() on a StreamExtractor class tailored for YouTube. It passes a URL, which is the YouTube page of the video.

When I run this, I get the following output:
$ ./gradlew run

> Task :appwrapper:run
Initialising
Downloading video
URL: https://www.youtube.com/watch?v=xvFZjo5PgG0
Video name: Rick Roll (Different link + no ads)
Uploader: Duran
Category: Entertainment
Likes: 167361
Views: 16866220
Content: https://rr1---sn-uigxx03-aige.googlevideo.com/videoplayback?expire
    =1740101145&ei=uYG3Z_uiBM2hp-oP9ZiJmAo&ip=92.40.170.100
    &id=o-AKIf20e0Ns4q-76XEVbZUy_J03FkxjUEdnV-2JIlPcYb&itag=18&source=youtube
    &requiressl=yes&xpc=EgVo2aDSNQ%3D%3D&met=1740079545%2C&mh=bl&mm=31%2C29
    &mn=sn-uigxx03-aige%2Csn-aigl6ned&ms=au%2Crdu&mv=m&mvi=1&pl=23&rms=au%2Cau
    &initcwndbps=1090000&bui=AUWDL3yKSAgQAEVY4tqV40xoTeMnyIkQ-FVPRnuzuYxBfqw
    -cOiBj0vT4a3RcTnvShBlnHPx5x0hdYtx&spc=RjZbSSYcKOnuHKrN6GGIqdfAdJXAv-1Oua6P
    _PR7IG324-f-zd5ZbgapDa0tKPiOzA&vprv=1&svpuc=1&mime=video%2Fmp4
    &ns=WgqxjXFJraikODL7BBUFz9QQ&rqh=1&cnr=14&ratebypass=yes&dur=7.685
    &lmt=1708738867597515&mt=1740079252&fvip=4&lmw=1&fexp=51326932&c=TVHTML5
    &sefc=1&txp=4530434&n=mORDIKBp9rP1UQ&sparams=expire%2Cei%2Cip%2Cid%2Citag
    %2Csource%2Crequiressl%2Cxpc%2Cbui%2Cspc%2Cvprv%2Csvpuc%2Cmime%2Cns%2Crqh
    %2Ccnr%2Cratebypass%2Cdur%2Clmt&lsparams=met%2Cmh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi
    %2Cpl%2Crms%2Cinitcwndbps&lsig=AGluJ3MwRQIhAJLLbhJvIq_01ibr5MzHnVaFuAS9j7G5
    RVHy_678ktfCAiA7LFBnPnZrgMGJANRJbxJ6GLJfL5I8x666zKpAHtvUZw%3D%3D
    &sig=AJfQdSswRQIhAMocRMCvpKEaqMcKT6mnU9M0fvY4AYNKYh0EGqKwl7asAiBVvWZSmDfanF
    SbX6OBgpJMH8q9ozm36-xbHVqHSvdNbQ==&cpn=P8sgdlqH3YiDM4MD
Completed

[Incubating] Problems report is available at: file:///home/flypig/dev/build/
    reports/problems/problems-report.html

Deprecated Gradle features were used in this build, making it incompatible with 
    Gradle 9.0.

You can use '--warning-mode all' to show the individual deprecation warnings 
    and determine if they come from your own scripts or plugins.

For more on this, please refer to https://docs.gradle.org/8.12.1/userguide/
    command_line_interface.html#sec:command_line_warnings in the Gradle 
    documentation.

BUILD SUCCESSFUL in 8s
6 actionable tasks: 1 executed, 5 up-to-date
The Content URL that gets spat out at the end points to a version of the YouTube video that can be streamed directly. Neat! I wouldn't bother trying to use the link above though as it's only temporarily active and, by the time you read this, will likely no longer work. But if we wanted a fresh one, we could run this code to generate it again.

That feels like a success and a good place to finish off for the day. Tomorrow I'm going to look at something completely different: running Java code on Sailfish OS.
11 Mar 2025 : Day 2 #
Yesterday we looked at the NewPipe app and briefly discussed the NewPipe Extractor. The app may be useful for considering some of the design decisions that the NewPipe team have made, but I don't expect it to be of much use from a code perspective.

The Extractor on the other hand... that's where the treasure is!

The Extractor is apparently already used in several "YouTube Download" tools: the sort of tool you give a URL and it downloads a video for you to view locally. But the Extractor is just a library, we need to build some scaffolding around it to make it useful.

So let's try that today. As I mentioned on the preamble, there's no Java virtual machine available from the official Sailfish OS repositories. So to begin with I'll be doing this all on my Linux laptop running Ubuntu 22.04.

First we clone the repository and move inside the local copy:
git clone git@github.com:llewelld/NewPipeExtractor.git
cd NewPipeExtractor
$ ls
LICENSE  README.md  build  build.gradle  checkstyle  extractor  gradle  gradlew
    gradlew.bat  jitpack.yml  settings.gradle  timeago-parser
Inside you can see it's made up of two sub-projects: extractor and timeago-parser. It's the former that we're really interested in. You can also see there's a gradle directory alongside a gradlew bootstrapping script.

NewPipe Extractor is set up to use Gradle for building. Gradle is commonly used for Android app development, but I've never seen it used for building things on Sailfish OS (I'm sure there are examples, but they must be pretty rare).

Nevertheless, everything is set up for us and a nice feature of Gradle is that you don't need to install Gradle to use it. The gradlew script will run out-of-the-box without the need for any additional infrastructure (the Ubuntu docker image I'm using seems to have come with a JDK already installed, so I'm just using that).

We can find out what tasks are available for us to use with the gradle build tool as follows. Here are the first few:
$ ./gradlew tasks
Starting a Gradle Daemon (subsequent builds will be faster)

> Task :tasks

------------------------------------------------------------
Tasks runnable from root project 'NewPipeExtractor'
------------------------------------------------------------

Application tasks
-----------------
run - Runs this project as a JVM application

Build tasks
-----------
assemble - Assembles the outputs of this project.
build - Assembles and tests this project.
[...]
The interesting one for us is assemble because this will build the code into a reusable jar archive. There's nothing special about jar archives, they're literally just ZIP archives that contain a MANIFEST.INF manifest file. But they're also how Java libraries get packaged up, so this is what we need.

Let's build ourselves the NewPipe Extractor library.
$ ./gradlew assemble

> Task :extractor:compileJava
Note: Some input files use unchecked or unsafe operations.
Note: Recompile with -Xlint:unchecked for details.

BUILD SUCCESSFUL in 6s
7 actionable tasks: 7 executed
We can see the results of this in the extractor/build/libs/ directory:
$ ls extractor/build/libs/
extractor-v0.24.5-sources.jar  extractor-v0.24.5.jar
$ file extractor/build/libs/extractor-v0.24.5.jar
extractor/build/libs/extractor-v0.24.5.jar: Zip archive data, at least v1.0 to 
    extract, compression method=deflate
Okay, we've downloaded and built the library, now we want to use it.

I'm not planning to directly use the library on Sailfish OS like this. But as pointed out by pherjung and thigg — two long-time fellow Sailfish OS enthusiasts — there are clever ways around this, for example using GraalVM. I'll return to this in a future post, but for now, I'm just going to stick to using the library on Ubuntu while I get familiar with it.

However we can't really make use of this library without also writing some code to make use of it. That'll be for tomorrow.
10 Mar 2025 : Day 1 #
Today we're moving on from yesterday's preamble to start work proper on getting NewPipe running on Sailfish OS.

Let's start by asking "What is NewPipe?" If you've used it, it'll need no explanation, for while it's a complex application both under-the-hood and from a user-interface perspective, its purpose is pretty straightforward.

NewPipe is a YouTube client. Except it's not exactly. It's also a SoundCloud client. And a media.ccc.de client. And a Bandcamp client. And a PeerTube client. Although I don't think it's actually a PeerTube client, more a client for accessing Framatube, which is a PeerTube instance.

The point is it allows access to various platforms for streaming and downloading audio and video. The backend, incidentally, allows plugins to be added for all sorts of sites, building on those I listed above. But as with the NewPipe app for Android, I'll be focusing on the five core services, starting with YouTube.

As Henry Hiles (QuadRadical) pointed out yesterday, there are other YouTube clients already available, including the rather nice Gtk4-based Pipeline app which is designed for use on Linux and even already known to work nicely on mobile Linux.

Until QuadRadical pointed it out I wasn't familiar with Pipeline, which is the main reason I chose NewPipe over it. But Pipeline is also quite different. It's written in Rust for a start, although that's absolutely not a reason to discount it. It's true that Rust isn't a language that lives comfortably on Sailfish OS, but there are examples of excellent apps for Sailfish OS written in Rust (vide Whisperfish) and with NewPipe being written in Java, it's in no better position to work on Sailfish OS. I'd say that Sailfish OS's support for Java is even poorer than its support for Rust.

But for full functionality Pipeline also relies on an external download utility, such as yt-dlp. That makes it less suitable for our purposes.

So Pipeline is also interesting and hopefully I'll get the chance to return to it. But since I'm already on this NewPipe track, I don't think Pipeline offers a strong enough reason to be diverted.

Let's look at the NewPipe Android app in more detail. Here are a few screenshots from the app, running under Android App Support on my Sailfish OS phone.
 
The four main tabs of the NewPipe Android app: Trending, What's New, Subscriptions and Bookmarked Playlists.


These screenshots show the four main tabs that are presented to the user when they open the app. These tabs are labelled Trending, What's New, Subscriptions and Bookmarked Playlists.

The Trending tab shows a list of audio tracks or videos that the service wants to show you for whatever reason. Presumably intended to represent media that's either popular or looks like it might become popular. But in practice I'm not sure what the algorithm is that's being used in the background.

Personally I have my YouTube history disabled, which means that when I visit the site on the Web I'm just presented with an empty page. But I know others like to be given suggestions for what to watch, in which case this tab will be a useful addition.

It's worth noting that this tab changes its name depending on which service is being used, but they're all variations on a theme: Trending, New and hot, Recent and Featured.

The What's New tab shows a chronological list of items for channels the user has explicitly subscribed to. When you visit a channel there's an overt Subscribe button you can select to subscribe to the channel's feed. The subscription is handled entirely locally — the fact is never transmitted to YouTube — and just means that the app will periodically check for new media released to the channel's feed. By default this is every five minutes, but this can be adjusted in the settings.

Subscriptions are much more useful for someone like me who's looking for a curated list of videos. And it all works nicely. Pulling the list down will trigger an update outside of the usual cadence.

Next is the Subscriptions tab. As you might imagine this shows all of the feeds that you've subscribed to. Just the feeds, not the content in the feeds. But you can of course just select one of them to drill down deeper and see the videos that have been released on the channel.

Finally we have the Bookmarked Playlists tab. I suppose technically the difference between a channel and a playlist is that anyone can create a playlist, whereas a channel feed is managed only by the channel owner. When you open a video you have the option to add it to one of your own playlists, or you can pull one of the playlists created by some other YouTube user. Either way they'll appear on this tab.

If you look carefully at the screenshots you'll also notice there's a little magnifying glass at the top right of the screen. Selecting this brings up a new page allowing you to search the entire service based on the text you enter. This is probably the most important functionality as far as I'm concerned. But all of these tabs are essentially aimed at allowing you to surface content in different ways.

So far we've concentrated mostly on YouTube, but as I mentioned above the app actually supports multiple services. They can be switched between, but only one service is active at a time.

A nice touch is that when you switch to a different service the colour of the user interface changes to reflect the fact, as you can see in the screenshots below.
 
Screenshots showing the main tab for four of the five services supported: SoundCloud, media.ccc.de, FramaTube and Bandcamp.


NewPipe does a decent job of maintaining consistency across the five services, even though they offer quite different types of media and categorisations. Part of the reason this is possible is because apart from search and accessing the media itself, much of the functionality is being provided by the app on your phone, rather than the backend service.

That's a consequence of the fact NewPipe is a read-only app. It accesses the media from the various services, but it doesn't authenticate to them, so there's no way for it to store data with the service. If you're looking for privacy then that's a feature, not a bug.

Another consequence of this is that although only one service can be active at a time, content you've subscribed to from one service is available even when you've switched to another. All of the subscriptions and playlists get mixed together into a single list, rather than being separated out by service. I really like this: it allows all of the most relevant content to be merged together and accessed easily.

Alright, let's dig a bit deeper into the user interface and find out what happens when we actually try to play some audio or video. The following screenshots show some of this functionality, but in practice there's such a wealth of options and user interface flows that there's no way I can describe all of it here.
 
Screenshots showing some of the app's functionality: the video player, video downloader, music player and the main menu.


Here we first see the video player with user comments below. As already mentioned, this is a read-only app so there's no way to add new comments. On this page there are actually two further tabs to replace comments with a list of related videos, or with the metadata related to the video.

At this point the user interface has become quite cluttered and if this is going to make it to Sailfish OS I feel it'll need to be simplified. At some point I'll have to do some proper design work to figure this all out.

From this page you can also download the video or audio to your local device, as shown in the second screenshot. This is critical functionality as far as I'm concerned. I like to download audio and video in advance while I'm at home over Wifi. Not only is this faster and cheaper than using a mobile connection, it also avoids the annoying situation when the mobile signal drops off and your video is left "buffering". That's something that happens surprisingly frequently when travelling by train in the UK.

There's also a nice music player as shown in the third screenshot. The interface is similar to that used for videos, which also means it's a bit too cluttered for my liking. But there's no shortage of juicy functionality.

Finally the screenshot on the right shows the main menu for when YouTube is the selected service. The little down arrow next to the "YouTube" text is an indication that you can switch service by tapping on the service name there.

The other menu entries are pretty self-explanatory. It's also worth noting there are nine items in this menu, which is far too many to work for a Sailfish-style pull-down menu. However the first four items are duplicates of the tabs so could be skipped and the last three could realistically be combined together leave just three items. That's more in line with what you might typical expect of a Sailfish OS menu.

It looks like this might all be workable.

I must admit that I'd not used the app before Schabi promoted it to me at FOSDEM. But it works perfectly using Android App Support on Sailfish OS and is well worth giving a go. Nonetheless the objective of this project is to make the Android app redundant for users of Sailfish OS.

This is all useful background material, but it's not actually the app itself that's interesting for us. You see the NewPipe devs have thoughtfully split the app into two parts: the user interface and the backend functionality.

The code for the former can be found in the aptly named NewPipe repository. But from a user-interface perspective Android is so different from Sailfish OS that it's unlikely we'd be able to make much use of this code.

It's the code for the latter, found in the NewPipe Extractor repository, that I'm hoping we'll be able to make better use of. This provides a Java library for searching, streaming and downloading media from the various services. It also provides an API for accessing the metadata, comments and other data associated with it.

The plan is to hook this library up to a Sailfish OS user interface built using Qt. We'll need to perform some API-boundary transmutation magic in order to get the Java library communicating effectively with the C++ front-end code. That'll be a bit of a challenge, but I'm hoping the challenge will also help make this a bit more interesting.

We'll take a deeper look at the NewPipe Extractor codebase tomorrow.
Comment
9 Mar 2025 : NewPipe dev diary #
Today I started a new project — a NewPipe-dev dary — which will chart my progress as I attempt to build a YouTube client based on NewPipe for Sailfish OS. The first preamble post is now up.
9 Mar 2025 : Preamble #
In the run-up to FOSDEM it got super-busy. I was preparing a talk, helping organise the Linux on Mobile stand, arranging a Sailfish Birds-of-a-Feather event, plus also working with my colleagues at the Turing on our conference arrangements too. The build up of work started all the way back in August, in fact.

So from August to February my focus outside of work has almost exclusively been on that. It sounds crazy, all leading up to just two days in February, but that's how it was.

But as the event approached I started to think about what I might do after FOSDEM. You know, once I got all of that glorious free time back! One obvious possibility was to start looking at the Gecko upgrade from ESR 91 to ESR 102, so I made a passing comment on the forum suggesting that I might pick up the work again after FOSDEM.

Quite quickly after this I heard that work had already been started on it behind the scenes. This is great news: more than anything else I believe Sailfish OS needs an up-to-date browser and anything that brings this closer has to be a good thing.

Since FOSDEM there's even been some public progress. New ESR102 branches have appeared in the embedlite-components and sailfish-browser repositories, each containing fresh commits. Unfortunately the really important changes are going to be in the gecko-dev repository and, at time of writing this, there's currently no new ESR102 branch there.

I'd love to help support the efforts and I'd also love for the work to happen in the open, but until something appears there, there's unfortunately no useful way for me to contribute. And in truth, that may be for the best. Coordinating a multi-developer project is a job in itself and often harder than just making the changes as a single developer.

So by the time I was behind the stand at FOSDEM I already knew I'd probably be looking for some other project to occupy my time.

You never know what sort of people you'll meet at FOSDEM. Working on a stand is, I honestly believe, a particularly special privilege. Everybody comes to you and shares their stories. You get to meet such an amazing range of fascinating people. It's self-selecting but otherwise unfiltered. I can honestly say that I met such wonderful people at FOSDEM, both new faces and old friends.

One unexpected interaction happened when one of the NewPipe developers came to the stand. There were several of the NewPipe team at FOSDEM, including Fabio Giovanazzi (Stypox), Fynn Godau (fynngodau) and Christian Schabesberger (Schabi). I think it was Schabi I spoke to and we had a really interesting conversation.

I hadn't heard of NewPipe before. It's a media-streaming client that gives access to content from the likes of YouTube, SoundCloud, PeerTube, Bandcamp and others. Unlike other clients NewPipe eschews the official service access APIs in favour of scraping the Web content directly or using reverse-engineered internal APIs. This allows the app to offer more privacy-respecting access.

"I like the sound of that", I thought.

But NewPipe is an Android app, which explains why I'd not heard of it. That means it's written primarily in Java. Outside of Android App Support, Java and Sailfish OS don't make great bedfellows. There's no Java virtual machine installed by default or available from the official repositories for example, which also means it would be a challenge getting a Java-based application through the Jolla Store review process.

Back to FOSDEM and I took a NewPipe leaflet, leaving it idly on the table amongst the other paraphernalia. As the day went on I was a little surprised about how many people commented on it. "Oh yes; I use NewPipe!" I heard at least a couple of times. This piqued my interest even more.

So I wondered: wouldn't it be neat if there was a version of NewPipe for Sailfish OS?

It's true that Sailfish OS already has a nice YouTube app in the form of MicroTube from Michał Szczepaniak. I like MicroTube, but I think it would be neat to have NewPipe functionality available as well. Unlike NewPipe MicroTube uses ytdl-core — written in JavaScript — for providing the underlying functionality for YouTube access. There's nothing wrong with that, but I was also taken with the NewPipe team's commitment to privacy, open source and keeping the software working in the face of the underlying YouTube API being updated.

So I've decided to have a go at writing a YouTube app using NewPipe. I'd say this is certainly going to be a smaller and less impactful project compared to the Gecko work. But I think it could be interesting for a number of reasons.

First, I don't see a lot of regular blogging happening about Sailfish OS app development. Don't get me wrong: Sailfish OS has a thriving app developer community, but mostly it doesn't seem to get serialised in prose.

Second, this is a technically unusual project, mixing C++ with Java and attempting to pull Android code into a Sailfish OS context. I think this will make it interesting to document.

Third, it may not result in the greatest utility in all the world, but NewPipe is a popular app and I think it has some functionality which would be nice to bring to Sailfish OS.

So why not? As with my Gecko diaries I plan to write about this daily. I'm aiming for a simple app: search YouTube then download or stream a video. My diary will run up until at least the first release, but I don't plan for this to be an Odyssey. Hopefully more of a scenic stroll in the park.

Just to be clear, it's not my intention to somehow replace or compete with existing apps like Microtube. Maybe in fact I'll just end up making some pull requests to Microtube after all this. But I don't see a problem in there being multiple YouTube clients available for Sailfish OS.

Tomorrow I'll take a look at the NewPipe Android app, before moving on to the technology that underpins it.

I'll post announcements about these diary entries on my Mastodon feed and after recent changes to this site, any replies to my Mastodon posts will also appear as comments here.

If you'd like to follow my progress, I'll be posting here every day and would love it if you fancy joining me.
Comment