Share My Creation B4J PyBridge Youtube Video Downloader

Project objective
This project demonstrates a seamless integration between B4J and Python through the PyBridge extension to enable online video and audio uploads.
It was designed as a modular base that any developer can adapt, enrich, and redistribute to create a complete tool.

Main features
Download video and audio content via yt_dlp, with configurable options (format, codec, quality, etc.).
Two-way interaction between B4J and Python through the use of the Py.RunCode(...) module.
Customize settings like output folder, media type, format, and playlist item selection.
Clean and modular code, easy to maintain and integrate into other projects.

This project is ideal as a starting point for:
Create a personal or community video/audio download utility
Integrate a multimedia processing module into a B4X application
Prototyping a simplified interface for yt-dlp without a command line
Introducing beginners to Python ⇄ B4X interoperability

Distribution and expansion
The code is freely distributable and modifiable. You can:
Add an error or history management system
Integrate a database or other local library
Add an options menu to set output quality or format
Doing completely what you want

Requirements
B4J with PyBridge installed
KeyValueStore to save paths
Library yt_dlp (pip install yt-dlp) - Python multimedia download library
FFmpeg in the system PATH for audio conversions and extractions

YT_DLP USAGE AND OPTIONS

Problem encountered
I have not been able to implement a progress bar.
I redirected the logs, but I only have access to them when the process is finished with Wait For

log redirection example:
py:
Py.ImportModule("io")
Py.ImportModule("sys")
.....
   buffer = io.StringIO()
   sys.stdout = buffer
....
sys.stdout = sys.__stdout__
return buffer.getvalue()


Private Sub btStart_Click
    ....
    Wait For (DoIt.Fetch) Complete (Result As PyWrapper)
    TextArea1.Text = Result.Value
    ....
End Sub

It would have been interesting to have something like this
B4X Py:
Sub Py_OutputReceived(Text As String)
    If Text.StartsWith("download") Then
        Dim pct As Float = Text.SubString(9).Trim
        ProgressBar1.Progress = pct
        lblStatus.Text = $"Download: ${pct}%"$
    Else
        Log(Text) ' To see other releases
    End If
End Sub

Sub Py_ErrorReceived(error As String)
    Log(error)
End Sub

Sub Py_ExecutionCompleted(Success As Boolean, Result As Object)
    Log(Success)
End Sub

Or maybe I don't know all the intricacies of PyBridge yet and it's actually possible to do it.
In this case, I would like to know the solution.

Have fun!
 

Attachments

  • Screenshot.png
    Screenshot.png
    10.2 KB · Views: 523
  • B4J_YouTubeDownloader.zip
    6.5 KB · Views: 41
Last edited:

Mariano Ismael Castro

Active Member
Licensed User
1753303744552.png



Hi, great work.
I've also tried this library before, and so far I've been able to add the playlist URL. I get the URL for each video and then click on any of them to download it.

I stopped because I wanted to show a download progress bar, but I couldn't do that.
 

pixet

Member
Licensed User
Longtime User
Try writing the value you get from the video position to a file, then read this value in time, even with a timer, and display the value in the bar.
 

pixet

Member
Licensed User
Longtime User
I'm not a Python expert, so I've never used it with Python, but I use this method when I run external processes in the shell that allow parameters,
so I can retrieve intermediate data between the start and end of the shell process.
You must make sure the file you're writing the value to isn't open exclusively. If so, it might return an error because it can't open
the file for reading if it's already open for writing.

To avoid this problem, you could try using the KeyValueStore method or a Database, which would allow multiple connections.
If you already use a database to manage your application, you could add a new table containing the value that updates as it's downloaded.

You might also consider downloading multiple data items without interference.

When you try this, I'd love to see the solution you've implemented in your code.
 
Last edited:

zed

Well-Known Member
Licensed User
I think redirecting the logs to a file and then reading the data with a timer is the best solution.
However, this is likely to make the application very heavy.
In addition, if the download is too fast, it will not be possible to have this data.
In the logs, we can see the progress, but sometimes it is directly at 100% in 1 second.

We could use something like this, a Python function that writes logs to a file in real time,
with the immediate flush option to allow B4J to read the logs as they are read.

Writes logs to the specified file, in real time. (The code has not been tested)
Python:
import time

def write_logs(file_path: str, number_logs: int, interval: float):
   
    with open(file_path, "a", encoding="utf-8") as logfile:
        for i in range(1, number_logs + 1):
            line = f"[{time.strftime('%H:%M:%S')}] Log number {i}\n"
            logfile.write(line)
            logfile.flush()
            time.sleep(interval)


if __name__ == "__main__":
    write_logs("logs.txt", number_logs=10, interval=1.0)

Args:
file_path (str): Path to the log file (ex: 'logs.txt')
number_logs (int): Number of lines to write
interval (float): Delay (in seconds) between each line

and in B4J, something like this:
B4J:
Sub tmrLogReader_Tick
    Dim logs As String = File.ReadString(File.DirApp, "logs.txt")
    If logs <> previousContent Then
       Dim newLogs As String = logs.SubString(previousContent.Length)
        Log(newLogs)
        previousContent = logs
    End If
End Sub
 

pixet

Member
Licensed User
Longtime User
Hmm, I was thinking of a counter (from 0 to 100 or similar) that I can manage with KeyValueStore (or shared variable),
sharing the value between Python and B4J.

I suggested using the file to have two independent threads, or even three threads:
1: the main process
2: the Python process
3: the thread that retrieves data from the file or from the shared variable(s) to process for the progress bar.

If managed with the Threading Library, I think it's feasible.
The library in question was created for B4A but also works well in B4J.

Threading library

B4J Question - Threading Library

 

zed

Well-Known Member
Licensed User
The Thread library was developed before the advent of form wait,
but in most cases, the summarized subscripts and Sleep are more than sufficient without the risk of blocking the user interface.
It's perfectly possible to run a Python script in the main thread and start reading the file via an asynchronous loop, without disrupting the user interface.
Something like this:
Launch Py.RunCode() in a resumable sub:
Sub LaunchScriptPython
    Py.RunCode("MyPythonCode()")
    Log("Python script launched.")
End Sub
Create a parallel task to read the file without blocking the UI:
Sub ReadFileLogs
    Do While True
        Dim contentLogs As String = File.ReadString(File.DirApp, "logs.txt")
        ' Updates the display :
        txtLogs.Text = contenuLogs

        ' short break before the next reading
        Sleep(1000)
    Loop
End Sub

And we can start it in parallel ::
Private Sub Start
    LaunchScriptPython
    ReadFileLogs ' This sub does not block
End Sub

Result
Your Python script runs normally.
The file is read in a loop every second.
The interface remains responsive, and you can display the logs in a TextArea, a Label, or a custom panel.
No true native thread is used, so there's no synchronization issue.
 

LucaMs

Expert
Licensed User
Longtime User
I really appreciate you sharing that project, @zed. Plus, it's a great opportunity to learn B4J+Python (even for myself; I've tried very little, but I think it has a great future).

I just wanted to write about a coincidence, just out of curiosity.
I developed a tiny B4J program specifically for downloading videos from YouTube. "Of course," it's exclusively B4J code (it uses (shell) yt-dlp_x86.exe and ffmpeg.exe).
What's the coincidence? The date I developed it:
1753354209446.png

June 22, 2025
 
  • Haha
Reactions: zed

Daestrum

Expert
Licensed User
Longtime User
Couldn't you use something like Erel describes in this thread for the progress bar
call B4J from Python
 

zed

Well-Known Member
Licensed User
Couldn't you use something like Erel describes in this thread for the progress bar
Excellent approach. I didn't know about this thread. But it might be the right solution.
Maybe we could do something like this:
Test:
bridge_instance.raise_event("OutputReceived", {"Buffer",buffer.getvalue()})

Py.SetEventMapping("OutputReceived", Me, "Py")

Private sub Py_OutputReceived(Args As Map)
value = Args.Get("Buffer")
end sub
 

zed

Well-Known Member
Licensed User
it's a great opportunity to learn B4J+Python
I started doing it in shell command.
Then I told myself that Erel hadn't done all that work for nothing.
That's how I got started. And to tell the truth, I find Python significantly faster than JAVA.
Plus, you can use Python libraries directly in your code. Which is definitely a plus.
So, if you want to have fun, don't hesitate for a second.
 

zed

Well-Known Member
Licensed User
The code has been modified to receive the output data.

Python:
def hook(d):
    print("Hook called") 

    if 'status' in d and d['status'] == 'downloading':
        message = f"{d.get('filename', '')}: {d.get('_percent_str', '')} - {d.get('_speed_str', '')}"
        print(message) 
        bridge_instance.raise_event("OutputReceived", {"data": message})
    
.....
'progress_hooks': [hook],
.....

The hook function in the Python script is a callback function used by yt_dlp to report the progress of the download.

Explanation:
def hook(d): Define a function that receives a dictionary d containing the download information.

if 'status' in d and d['status'] == 'downloading': If the status is "downloading" to avoid acting on other steps (like "finished" or "error").

message = f"...": we construct a readable string of characters: the name of the file, the percentage, and the download speed.

bridge_instance.raise_event("OutputReceived", {"data": message}) This is the magic key: trigger an event to B4J using bridge_instance, and send the message via the OutputReceived event.


and in B4J
B4J:
Private Sub Py_OutputReceived(Args As Map)
    Dim value As String = Args.Get("data")
    txtLogs.Text = txtLogs.Text & value & CRLF
  
    Dim raw As String = Args.Get("data") ' Exemple: "video.mp4: 45.3% - 1.2MiB/s"
    Dim pourcent As Float = ExtractPercentage(raw)
    ProgressBar1.Progress = pourcent
End Sub

Private Sub ExtractPercentage(logLine As String) As Float
    Dim m As Matcher = Regex.Matcher("\b(\d{1,3}(\.\d+)?)%",logLine)
    If m.Find Then Return m.Group(1)
    Return 0
End Sub

We can see in the logs that the line was successfully added.
But I still receive a packet at the end of the process.
 

Attachments

  • Update.png
    Update.png
    91.4 KB · Views: 37
Last edited:
Top