├── .gitattributes ├── LICENSE ├── README.md ├── daso_installer_no_python.bat ├── daso_installer_python.bat ├── daso_script.ps1 ├── daso_script_python.ps1 ├── img ├── porcodio_1.png └── porcodio_2.png ├── requirements.txt └── src ├── DASO.py ├── __main__.py ├── __pycache__ └── DASO.cpython-310.pyc └── daso.bat /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Joshua | Tommaso 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 |
DASO 3 |

4 | 5 |

6 | DASO - Stream music from YouTube directly from your terminal. 7 |

8 | 9 |
10 | DASO Interface Screenshot 11 |
12 | 13 | ## Table of Contents 14 | 15 | - [Installation](#-installation) 16 | - [Usage](#-usage) 17 | - [Keybinds](#-keybinds) 18 | - [Important Notes](#-important-notes) 19 | - [Contributing](#-contributing) 20 | - [License](#-license) 21 | 22 | ## 🚀 Installation 23 | 24 | DASO is easy to install with its dedicated installer for Windows. Follow these steps: 25 | 26 | 1. **Choose Your Installer**: 27 | - `daso_installer_no_python.bat`: If you already have Python installed. 28 | - `daso_installer_python.bat`: If you don't have Python. [Install Python here](https://www.python.org/downloads/) in case it doesn't work. 29 | 2. **Run the Installer**: Double-click the chosen .bat file in the main folder. Follow the instructions. 30 | 3. **Installation Complete**: DASO is now ready to use! 31 | 32 | > 📝 **Note**: Currently, DASO is not available for Linux or Mac. There are no plans for these platforms at the moment. 33 | 34 | ## 🎵 Usage 35 | 36 | The DASO installer makes it so that you can run it from anywhere in your terminal. Basically, it adds a new command to your terminal, `daso`, which you can use to run like this: 37 | 38 | ```bash 39 | daso [song name] 40 | ``` 41 | 42 | For example: 43 | 44 | ```bash 45 | daso never gonna give you up 46 | ``` 47 | 48 | DASO uses the YouTube API to search for the song you want to listen to, so you can use any name you want, as long as it exists on YouTube. Remember tho, to provide a name, else you will get an error. 49 | 50 | ### 🎹 Keybinds 51 | 52 | DASO has some keybinds that you can use to control it while it's running. Here's a list of them: 53 | 54 | - `space` to pause/play the song 55 | - `ctrl + c` to stop the song and exit DASO 56 | - `arrow up` to increase the volume 57 | - `arrow down` to decrease the volume 58 | 59 |
60 | Your Image Description 61 |
62 | 63 | ## ❗ Important 64 | 65 | The neat thing about DASO is that it doesn't download the song you want to listen to, it just streams it from YouTube. Another important note is that you can't listen to Lives _for now_, I'm currently figuring out how to implement this feature. If you know how to, please feel free to [contribute](#contributing)! 66 | 67 | ## 🤝 Contributing 68 | 69 | I welcome all types of contributions, including documentation, code, tests, suggestions, etc.... Other than that, you can also open an issue if you find a bug or if you have a suggestion, I'll try to answer as soon as possible. 70 | 71 | ## 📝 License 72 | 73 | [MIT](LICENSE) -------------------------------------------------------------------------------- /daso_installer_no_python.bat: -------------------------------------------------------------------------------- 1 | PowerShell -NoProfile -ExecutionPolicy Bypass -File daso_script.ps1 2 | -------------------------------------------------------------------------------- /daso_installer_python.bat: -------------------------------------------------------------------------------- 1 | PowerShell -NoProfile -ExecutionPolicy Bypass -File daso_script_python.ps1 2 | -------------------------------------------------------------------------------- /daso_script.ps1: -------------------------------------------------------------------------------- 1 | # Check if the script is running as administrator 2 | if (-NOT ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) { 3 | # Relaunch the script with administrator privileges 4 | $arguments = "& '" + $myinvocation.mycommand.definition + "'" 5 | Start-Process powershell -Verb runAs -ArgumentList $arguments 6 | Exit 7 | } 8 | 9 | # General output text 10 | Write-Output "Welcome to the DASO installer!" 11 | 12 | # Get the directory of the script 13 | $scriptPath = Split-Path -Parent -Path $MyInvocation.MyCommand.Definition 14 | $srcPath = Join-Path -Path $scriptPath -ChildPath "src" 15 | 16 | # Add src directory to system PATH 17 | $envPath = [System.Environment]::GetEnvironmentVariable("Path", [System.EnvironmentVariableTarget]::Machine) 18 | if (-not ($envPath.Split(';') -contains $srcPath)) { 19 | $newPath = $envPath + ";" + $srcPath 20 | [System.Environment]::SetEnvironmentVariable("Path", $newPath, [System.EnvironmentVariableTarget]::Machine) 21 | Write-Output "The directory has been added to your system PATH." 22 | } else { 23 | Write-Output "The directory is already in your system PATH." 24 | } 25 | 26 | # Prompt the user to download VLC 27 | Write-Output "To complete the installation, please download VLC Media Player." 28 | $vlcDownloadUrl = "https://www.videolan.org/vlc/download-windows.html" 29 | Start-Process "chrome.exe" $vlcDownloadUrl # This opens the URL in Google Chrome. You can change the browser if needed. 30 | 31 | Read-Host -Prompt "Press Enter to exit setup..." # Wait for user input before closing the window 32 | -------------------------------------------------------------------------------- /daso_script_python.ps1: -------------------------------------------------------------------------------- 1 | # Check if the script is running as administrator 2 | if (-NOT ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) { 3 | # Relaunch the script with administrator privileges 4 | $arguments = "& '" + $myinvocation.mycommand.definition + "'" 5 | Start-Process powershell -Verb runAs -ArgumentList $arguments 6 | Exit 7 | } 8 | 9 | # General output text 10 | Write-Output "Welcome to the DASO installer!" 11 | 12 | # Check for Python installation 13 | $pythonInstalled = $false 14 | try { 15 | Start-Process python --ArgumentList '--version' -Wait -NoNewWindow 16 | $pythonInstalled = $true 17 | } catch {} 18 | 19 | if (-not $pythonInstalled) { 20 | Write-Output "Python is not installed. Starting installation..." 21 | 22 | # Specify the Python version to install 23 | $pythonVersion = "3.10.0" # Change this to what version you want, I advice 3.10.0 as I've written the program in that 24 | $installerUrl = "https://www.python.org/ftp/python/$pythonVersion/python-$pythonVersion-amd64.exe" 25 | 26 | # Download the Python installer 27 | $installerPath = "$env:TEMP\python-installer.exe" 28 | Invoke-WebRequest -Uri $installerUrl -OutFile $installerPath 29 | 30 | # Install Python silently 31 | Start-Process $installerPath -ArgumentList '/quiet InstallAllUsers=1 PrependPath=1' -Wait 32 | 33 | Write-Output "Python has been installed." 34 | } 35 | 36 | # Get the directory of the script 37 | $scriptPath = Split-Path -Parent -Path $MyInvocation.MyCommand.Definition 38 | $srcPath = Join-Path -Path $scriptPath -ChildPath "src" 39 | 40 | # Add src directory to system PATH 41 | $envPath = [System.Environment]::GetEnvironmentVariable("Path", [System.EnvironmentVariableTarget]::Machine) 42 | if (-not ($envPath.Split(';') -contains $srcPath)) { 43 | $newPath = $envPath + ";" + $srcPath 44 | [System.Environment]::SetEnvironmentVariable("Path", $newPath, [System.EnvironmentVariableTarget]::Machine) 45 | Write-Output "The directory has been added to your system PATH." 46 | } else { 47 | Write-Output "The directory is already in your system PATH." 48 | } 49 | 50 | # Prompt the user to download VLC 51 | Write-Output "To complete the installation, please download VLC Media Player." 52 | $vlcDownloadUrl = "https://www.videolan.org/vlc/download-windows.html" 53 | Start-Process "chrome.exe" $vlcDownloadUrl # This opens the URL in Google Chrome. You can change the browser if needed. 54 | 55 | Read-Host -Prompt "Press Enter to exit setup..." # Wait for user input before closing the window 56 | -------------------------------------------------------------------------------- /img/porcodio_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JoshuaKasa/DASO/0a69c088b19a5767030d8fa54d8732969a671ae7/img/porcodio_1.png -------------------------------------------------------------------------------- /img/porcodio_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JoshuaKasa/DASO/0a69c088b19a5767030d8fa54d8732969a671ae7/img/porcodio_2.png -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | anyio==4.2.0 2 | certifi==2024.7.4 3 | colorama==0.4.6 4 | exceptiongroup==1.2.0 5 | h11==0.14.0 6 | httpcore==1.0.2 7 | httpx==0.26.0 8 | idna==3.7 9 | keyboard==0.13.5 10 | markdown-it-py==3.0.0 11 | mdurl==0.1.2 12 | prompt-toolkit==3.0.36 13 | Pygments==2.17.2 14 | python-vlc==3.0.20123 15 | pytube==15.0.0 16 | questionary==2.0.1 17 | rich==13.7.0 18 | sniffio==1.3.0 19 | tqdm==4.66.1 20 | typing_extensions==4.9.0 21 | wcwidth==0.2.13 22 | youtube-search-python==1.6.6 -------------------------------------------------------------------------------- /src/DASO.py: -------------------------------------------------------------------------------- 1 | from pytube import YouTube 2 | from youtubesearchpython import VideosSearch 3 | import time 4 | import vlc 5 | import keyboard 6 | 7 | def format_time(seconds): 8 | """Convert seconds to MM:SS format.""" 9 | minutes, seconds = divmod(int(seconds), 60) 10 | return f"{minutes:02d}:{seconds:02d}" 11 | 12 | def stream_youtube_audio(url): 13 | # Fetching the YouTube video 14 | yt = YouTube(url) 15 | audio_stream = yt.streams.filter(only_audio=True).order_by('abr').first() # Highest bitrate 16 | audio_url = audio_stream.url # Get the audio url 17 | 18 | # Using VLC to play the audio 19 | player = vlc.MediaPlayer(audio_url) 20 | player.play() 21 | time.sleep(1) # Wait for player to start 22 | 23 | is_playing = True # Variable to track play/pause state 24 | volume = 100 # Initial volume set to 100% 25 | player.audio_set_volume(volume) # Set the initial volume 26 | 27 | try: 28 | while True: 29 | time.sleep(0.1) # A shorter sleep time for more responsive keyboard interaction 30 | 31 | # Adjust volume with up/down arrows 32 | if keyboard.is_pressed('down'): 33 | volume = max(0, volume - 5) # Decrease volume, but not less than 0% 34 | player.audio_set_volume(volume) 35 | while keyboard.is_pressed('down'): 36 | time.sleep(0.1) 37 | 38 | if keyboard.is_pressed('up'): 39 | volume = min(100, volume + 5) # Increase volume, but not more than 100% 40 | player.audio_set_volume(volume) 41 | while keyboard.is_pressed('up'): 42 | time.sleep(0.1) 43 | 44 | # Check if spacebar is pressed for play/pause 45 | if keyboard.is_pressed('space'): 46 | if is_playing: 47 | player.pause() 48 | is_playing = False 49 | print("[Paused]", end='\r') 50 | else: 51 | player.play() 52 | is_playing = True 53 | print("[Playing]", end='\r') 54 | while keyboard.is_pressed('space'): # Wait until space is released 55 | time.sleep(0.1) 56 | 57 | if is_playing: 58 | # We can print over the previous line using \r and end='\r', which is the carriage return character 59 | current_time = player.get_time() // 1000 60 | total_time = yt.length 61 | print(f"[Playing] {format_time(current_time)}/{format_time(total_time)} - Volume: {volume}%", end='\r') 62 | 63 | except KeyboardInterrupt: 64 | pass # Press Ctrl+C to stop 65 | finally: 66 | print() # Ensure the next print starts on a new line 67 | player.stop() # Stop the player 68 | 69 | 70 | def search_yt_audio_from_keyword(keyword: str) -> str | dict: 71 | # Search the keyword on YouTube 72 | videosSearch = VideosSearch(keyword, limit=10) 73 | result = videosSearch.result() 74 | 75 | return result 76 | -------------------------------------------------------------------------------- /src/__main__.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import DASO 4 | from rich.console import Console 5 | from rich.table import Table 6 | from colorama import Fore, Style 7 | import questionary 8 | from tqdm import tqdm 9 | import time 10 | 11 | # Initialize console 12 | console = Console() 13 | 14 | def truncate_string(s: str, max_length: int) -> str: 15 | """Truncate a string to a maximum length and add '...' if truncated. 16 | 17 | Args: 18 | s (str): The string to truncate. 19 | max_length (int): The maximum length of the truncated string. 20 | 21 | Returns: 22 | str: The truncated string. 23 | """ 24 | return s if len(s) <= max_length else s[:max_length-3] + '...' 25 | 26 | def main(arg: str) -> None: 27 | """Main function of the program. 28 | 29 | Args: 30 | arg (str): The search keyword. 31 | """ 32 | result = DASO.search_yt_audio_from_keyword(arg) 33 | print(f"\nShowing results for '{arg}':\n") 34 | 35 | # Set fixed widths for each column 36 | title_width = 30 37 | duration_width = 10 38 | views_width = 15 39 | 40 | table = Table(show_header=True, header_style="bold magenta") 41 | table.add_column("No.", style="dim cyan", width=6) 42 | table.add_column("Title", width=title_width, overflow="ellipsis") 43 | table.add_column("Duration", justify="right", width=duration_width) 44 | table.add_column("Views", justify="right", width=views_width) 45 | 46 | for i, video in enumerate(result['result']): 47 | video_title = truncate_string(video.get('title', 'No Title'), title_width) 48 | video_duration = truncate_string(video.get('duration', 'N/A'), duration_width) 49 | video_view_count = truncate_string(video.get('viewCount', {}).get('short', 'N/A'), views_width) 50 | 51 | if video_view_count is not None and isinstance(video_view_count, str): 52 | video_view_count = video_view_count.replace('views','') 53 | else: 54 | video_view_count = 'N/A' # Default value in case of None or non-string 55 | 56 | table.add_row(str(i + 1), video_title, video_duration, video_view_count) 57 | 58 | console.print(table) 59 | 60 | questions = [ 61 | { 62 | 'type': 'input', 63 | 'name': 'choice', 64 | 'message': 'Choose a video to play:', 65 | } 66 | ] 67 | 68 | choice = questionary.select( 69 | "Choose a video to play:", 70 | choices=[f"{i+1}. {video['title']}" for i, video in enumerate(result['result'])] 71 | ).ask() 72 | 73 | # Convert choice to index 74 | choice_index = int(choice.split('.')[0]) - 1 75 | url = result['result'][choice_index]['link'] 76 | 77 | try: 78 | DASO.stream_youtube_audio(url) 79 | except Exception as e: 80 | print(f'An error occurred while trying to play the audio, {e}') 81 | 82 | if __name__ == '__main__': 83 | argument = ' '.join(sys.argv[1:]) # Get the arguments passed to the program 84 | if argument == '': 85 | print('You must provide a search keyword as a argument.') 86 | print('Example: DASO never gonna give you up') 87 | sys.exit(1) 88 | 89 | main(argument) 90 | sys.exit(0) # Exit without errors 91 | -------------------------------------------------------------------------------- /src/__pycache__/DASO.cpython-310.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JoshuaKasa/DASO/0a69c088b19a5767030d8fa54d8732969a671ae7/src/__pycache__/DASO.cpython-310.pyc -------------------------------------------------------------------------------- /src/daso.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | cd /d "%~dp0" 3 | python __main__.py %* 4 | --------------------------------------------------------------------------------