├── requirements.txt ├── .gitignore ├── icon.ico ├── saavn-cli.jpg ├── latest-version ├── .circleci ├── release.md └── config.yml ├── PKGBUILD ├── LICENSE ├── readme.md └── saavn-cli.py /requirements.txt: -------------------------------------------------------------------------------- 1 | requests==2.27.1 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | build 2 | dist 3 | *.spec 4 | pkg/ 5 | saavn-cli/ 6 | src/ -------------------------------------------------------------------------------- /icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wiz64/saavn-cli/HEAD/icon.ico -------------------------------------------------------------------------------- /saavn-cli.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wiz64/saavn-cli/HEAD/saavn-cli.jpg -------------------------------------------------------------------------------- /latest-version: -------------------------------------------------------------------------------- 1 | {"version":"1.2.0","versionCode":"120","download":"https://github.com/wiz64/saavn-cli/releases"} 2 | -------------------------------------------------------------------------------- /.circleci/release.md: -------------------------------------------------------------------------------- 1 | # saavn-cli binary builds 2 | note - 3 | - `linux-amd64` for Linux 64bit 4 | - `saavn-cli.exe` for Windows 64bit 5 | - `linux-arm64` for Arm devices, Termux/Android, Raspberry pi 6 | - `darwin-amd64` for Macos(intel-based) 7 | --- 8 | `unzip saavn-cli*.zip` and `chmod +x saavn-cli` -------------------------------------------------------------------------------- /PKGBUILD: -------------------------------------------------------------------------------- 1 | # Maintainer: wiz64 2 | pkgname=saavn-cli 3 | _pkgname=saavn-cli 4 | pkgver=1.1.3.r3.gd73ad9d 5 | pkgrel=1 6 | pkgdesc="Command Line tool to search, download MP3 songs from Saavn Library. Open-source, and High Quality Music" 7 | arch=('any') 8 | url="https://github.com/wiz64/${_pkgname}" 9 | license=('MIT') 10 | provides=($_pkgname) 11 | conflicts=($_pkgname) 12 | depends=('python-pip') 13 | makedepends=('git') 14 | source=("$pkgname::git+https://github.com/wiz64/saavn-cli.git") 15 | md5sums=('SKIP') 16 | 17 | pkgver() { 18 | cd $pkgname 19 | git describe --tags --long | sed -r -e 's,^[^0-9]*,,;s,([^-]*-g),r\1,;s,[-_],.,g' 20 | } 21 | 22 | package() { 23 | cd $pkgname 24 | #make DESTDIR="$pkgdir" install 25 | python -m pip install -r requirements.txt 26 | python -m pip install pyinstaller 27 | python -m PyInstaller --icon=icon.ico --onefile saavn-cli.py -n saavn-cli 28 | chmod 777 dist/saavn-cli 29 | cd dist 30 | install -D -m755 saavn-cli "$pkgdir/usr/bin/saavn-cli" 31 | } 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 wiz64 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 | ## saavn-cli - Command-Line Music Downloader 2 | 3 | Search, Download and Play your favorite songs right away from the command-line. High-Quality MP3 Files upto 320kbps bitrate with Metadata.
4 | 5 | # Details 6 | 7 | ## [Download](https://github.com/wiz64/saavn-cli/releases) 8 |
9 | 10 | > Developer : [@wiz64](https://github.com/wiz64)
11 | > Status : `In Development`
12 | > Version : `v 1.2.0`
13 | > Last Updated : `April 2023`
14 | 15 | --- 16 | Written in - Python
17 | Compatible with Linux, Windows, Android(Termux) & MacOS 18 | 19 | # Features - 20 | - ❤️ Free and Open Source 21 | - 📙 A large library of tracks 22 | - 🚀 Search and Download tracks directly 23 | - ⚙️ Cross Platform - Linux, Windows, MacOS, Android(termux) 24 | - 🎶 Upto 320kbps MP3 files with Metadata 25 | - 🎧 Download Multiple tracks at once 26 | 27 | # Usage 28 | Quickstart / Compiled 29 | - [Install ffmpeg](#install-ffmpeg) 30 | - Download, Extract zip and run 31 | ``` 32 | chmod +x saavn-cli 33 | ./saavn-cli search english songs 34 | ``` 35 | Enter comma-seperated numbers for the tracks you want to download - `1,4,12,15` 36 | ..There you go ! 37 | 38 | 39 | 40 | --- 41 | - Python Script 42 | 43 | Universal : 44 | ``` 45 | pip install -r requirements.txt 46 | python saavn-cli.py search english songs 47 | ``` 48 | 49 | # How it works ? 50 | 51 | >When a user runs the script to search or download songs,
The script requests download links, album art, album details, etc from the unofficial API. It downloads the raw files and then compiles them using ffmpeg. 52 | 53 | ### Requirements 54 | 55 | - ffmpeg 56 | - Python (v3) 57 | 58 | 59 | #### Install FFMPEG 60 |
To check if ffmpeg is properly installed, run
61 | `ffmpeg -version`
62 | [Download ffmpeg](https://ffmpeg.org/download.html)
63 | - Termius/Linux : `sudo apt install ffmpeg`
64 | - MacOS (Homebrew) : `brew install ffmpeg`
check [brew.sh](https://brew.sh)
65 | - Windows users can copy `ffmpeg.exe` to `C:\Windows\System32` or any other `$PATH` Directory 66 | 67 | 68 | --- 69 | 70 | ### Compiling Binary : 71 | With `pyinstaller` 72 | 73 | `pip install -U pyinstaller` 74 | `python3 -m PyInstaller --icon=icon.ico --onefile saavn-cli.py -n saavn-cli` 75 | 76 | The Executable file will be saved to `dist` folder 77 |
78 | 79 | ## Argument Parsing 80 | Example Command : 81 | ``` 82 | ./saavn-cli search:160 English Songs 83 | ``` 84 | - Here `saavn-cli` is argv[0], the script entry point 85 | - `search:160` is argv[1], action and bitrate option, seperated by `: colon` as `ACTION:BITRATE`.
86 | - Action is Necessary but Bitrate is optional, 320 by default. 87 | - `English Songs` - Rest Arguments are "terms" used to query the API in search action or Links/IDs seperated by spacing in download mode 88 | # Actions 89 | ## Search 90 | To search for songs available on Saavn and download MP3 to current directory.
91 | Syntax : `saavn-cli `
92 | Argument : `s or search`
93 | Bitrate can be added optionally. 94 | Example: 95 | ``` 96 | ./saavn-cli s:160 DJ Snake 97 | ``` 98 | --- 99 | 100 | ## Multi Link Downloading (upcoming) 101 | 102 | ### From saavn song links or IDs :
103 | 104 | 105 | Syntax : `saavn-cli download LINK1 LINK2 ID1 ID2`
106 | Argument : `d or download`
107 | Bitrate can be added optionally.
108 | Supports Multiple Links/IDs
109 | Example : 110 | 111 | ``` 112 | saavn-cli download IEBQ7- DFEHNB- SJADKEi 113 | ``` 114 | --- 115 | 116 | ## Bitrate Settings 117 | (Optional) To specify bitrate, pass the desired bitrate to right of `:` after action.
118 | Supported Values : `320 (default), 160, 96, 48, 12`
119 | Example: 120 | ``` 121 | saavn-cli search:96 Magneta Riddim 122 | ``` 123 | --- 124 | ## Check for Updates 125 | To check for updates, run command 126 | ``` 127 | saavn-cli update 128 | ``` 129 | 130 | --- 131 | 132 | 133 | ### Todo - 134 | - adding link-download support 135 | - album, artist search 136 | 137 | # Footnotes 138 | I dedicate this project to a special one. Any guess who are they ? 139 | 140 | Anyone is free to contribute to this project, fixing bugs, optimising code, improving documentation, testing, feedback, etc. 141 | 142 | # License 143 | Copyright © 2022 wiz64 144 | 145 | The source code of this tool has been licensed under `MIT License`, Read the LICENSE File for more info. 146 | 147 | # Copyright Disclaimer 148 | I am not responsible for anything related to Third-Party copyright holders, This script comes with absolutely no warranties. Kindly use at your own risk.
We do not host or serve the Music files on our servers or accounts.
149 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | # Use the latest 2.1 version of CircleCI pipeline process engine. 2 | # See: https://circleci.com/docs/2.0/configuration-reference 3 | version: 2.1 4 | #: Define a job to be invoked later in a workflow. 5 | # See: https://circleci.com/docs/2.0/configuration-reference/#jobs 6 | 7 | jobs: 8 | setup: 9 | machine: 10 | image: ubuntu-2004:202101-01 11 | resource_class: arm.medium 12 | # Add steps to the job 13 | # See: https://circleci.com/docs/2.0/configuration-reference/#steps 14 | steps: 15 | - checkout 16 | - run: 17 | name: "Setup Workspace" 18 | command: | 19 | echo "Creating Workspace Directory" 20 | - persist_to_workspace: 21 | root: . 22 | paths: 23 | - . 24 | 25 | build-linux-arm64: 26 | # Specify the execution environment. You can specify an image from Dockerhub or use one of our Convenience Images from CircleCI's Developer Hub. 27 | # See: https://circleci.com/docs/2.0/configuration-reference/#docker-machine-macos-windows-executor 28 | machine: 29 | image: ubuntu-2004:202101-01 30 | resource_class: arm.medium 31 | # Add steps to the job 32 | # See: https://circleci.com/docs/2.0/configuration-reference/#steps 33 | steps: 34 | - attach_workspace: 35 | # Must be absolute path or relative path from working_directory 36 | at: . 37 | - run: 38 | name: "Building for arm64" 39 | command: | 40 | cd $CIRCLE_WORKING_DIRECTORY && 41 | sudo apt update -y && 42 | sudo apt install software-properties-common -y && 43 | sudo add-apt-repository ppa:deadsnakes/ppa -y && 44 | sudo apt install python3.9-dev -y && 45 | sudo apt install python-dev -y && 46 | sudo apt install pip -y && 47 | python3.9 -m pip install -r requirements.txt && 48 | python3.9 -m pip install pyinstaller && 49 | python3.9 -m PyInstaller --icon=icon.ico --onefile saavn-cli.py -n saavn-cli && 50 | chmod 777 dist/saavn-cli && 51 | cd dist && 52 | zip saavn-cli-linux-arm64.zip saavn-cli 53 | rm saavn-cli 54 | - persist_to_workspace: 55 | root: . 56 | paths: 57 | - dist 58 | 59 | 60 | build-linux-amd64: 61 | # Specify the execution environment. You can specify an image from Dockerhub or use one of our Convenience Images from CircleCI's Developer Hub. 62 | # See: https://circleci.com/docs/2.0/configuration-reference/#docker-machine-macos-windows-executor 63 | machine: 64 | image: ubuntu-2004:202010-01 65 | resource_class: medium 66 | # Add steps to the job 67 | # See: https://circleci.com/docs/2.0/configuration-reference/#steps 68 | steps: 69 | - attach_workspace: 70 | # Must be absolute path or relative path from working_directory 71 | at: . 72 | - run: 73 | name: "Building for Linux amd64" 74 | command: | 75 | sudo apt update -y && 76 | sudo apt install software-properties-common -y && 77 | sudo add-apt-repository ppa:deadsnakes/ppa -y && 78 | sudo apt install python3.9-dev -y && 79 | sudo apt-get install python3.9-distutils && 80 | sudo apt install pip -y && 81 | python3.9 -m pip install -r requirements.txt && 82 | python3.9 -m pip install pyinstaller && 83 | python3.9 -m PyInstaller --icon=icon.ico --onefile saavn-cli.py -n saavn-cli && 84 | chmod 777 dist/saavn-cli && 85 | cd dist && 86 | zip saavn-cli-linux-amd64.zip saavn-cli 87 | rm saavn-cli 88 | - persist_to_workspace: 89 | root: . 90 | paths: 91 | - dist 92 | build-win-amd64: # name of your job 93 | resource_class: 'windows.medium' 94 | machine: 95 | image: 'windows-server-2022-gui:current' 96 | shell: 'cmd.exe' 97 | steps: 98 | # Commands are run in a Windows virtual machine environment 99 | - attach_workspace: 100 | # Must be absolute path or relative path from working_directory 101 | at: . 102 | - run: pip install pyinstaller && pip install -r requirements.txt && pyinstaller --icon=icon.ico --onefile -n saavn-cli saavn-cli.py 103 | - persist_to_workspace: 104 | root: . 105 | paths: 106 | - dist 107 | build-darwin-amd64: 108 | macos: 109 | xcode: 13.4.1 110 | environment: 111 | HOMEBREW_NO_AUTO_UPDATE: 1 112 | steps: 113 | # Commands will execute in macOS container 114 | - attach_workspace: 115 | # Must be absolute path or relative path from working_directory 116 | at: . 117 | - run: | 118 | xcodebuild -version 119 | python3 -m pip install -r requirements.txt 120 | python3 -m pip install pyinstaller 121 | python3 -m PyInstaller --icon=icon.ico --onefile saavn-cli.py -n saavn-cli 122 | ls -lh dist 123 | cd dist 124 | zip saavn-cli-darwin-amd64.zip saavn-cli 125 | rm saavn-cli 126 | #install deps and compile 127 | - persist_to_workspace: 128 | root: . 129 | paths: 130 | - dist 131 | deploy: 132 | # Specify the execution environment. You can specify an image from Dockerhub or use one of our Convenience Images from CircleCI's Developer Hub. 133 | # See: https://circleci.com/docs/2.0/configuration-reference/#docker-machine-macos-windows-executor 134 | machine: 135 | image: ubuntu-2004:202010-01 136 | resource_class: medium 137 | 138 | # Add steps to the job 139 | # See: https://circleci.com/docs/2.0/configuration-reference/#steps 140 | steps: 141 | - attach_workspace: 142 | # Must be absolute path or relative path from working_directory 143 | at: . 144 | 145 | - run: 146 | name: "Deploy to Releases page" 147 | # get all artifacts from previous builds and publish them 148 | command: | 149 | ls dist 150 | sudo apt install jq 151 | ver=$(jq .version latest-version -r) 152 | VERSION="v$ver" 153 | wget https://github.com/tcnksm/ghr/releases/download/v0.16.0/ghr_v0.16.0_linux_amd64.tar.gz 154 | tar -xvf ghr_v0.16.0_linux_amd64.tar.gz 155 | chmod 777 ghr_v0.16.0_linux_amd64/ghr 156 | releasenote=$(cat .circleci/release.md) 157 | mv ghr_v0.16.0_linux_amd64/ghr ghr 158 | ./ghr -t ${GITHUB_TOKEN} -u ${CIRCLE_PROJECT_USERNAME} -r ${CIRCLE_PROJECT_REPONAME} -c ${CIRCLE_SHA1} -delete -b "${releasenote}" ${VERSION} ./dist/ 159 | 160 | # Invoke jobs via workflows 161 | # See: https://circleci.com/docs/2.0/configuration-reference/#workflows 162 | workflows: 163 | build-saavn-cli: 164 | jobs: 165 | - setup: 166 | filters: 167 | # ignore any commit on any branch by default 168 | branches: 169 | ignore: /.*/ 170 | # only act on version tags 171 | tags: 172 | only: /^v[0-9]+(\.[0-9]+)*$/ 173 | 174 | - build-linux-arm64: 175 | requires: 176 | - setup 177 | filters: 178 | # ignore any commit on any branch by default 179 | branches: 180 | ignore: /.*/ 181 | # only act on version tags 182 | tags: 183 | only: /^v[0-9]+(\.[0-9]+)*$/ 184 | 185 | - build-darwin-amd64: 186 | requires: 187 | - setup 188 | filters: 189 | # ignore any commit on any branch by default 190 | branches: 191 | ignore: /.*/ 192 | # only act on version tags 193 | tags: 194 | only: /^v[0-9]+(\.[0-9]+)*$/ 195 | 196 | - build-win-amd64: 197 | requires: 198 | - setup 199 | filters: 200 | # ignore any commit on any branch by default 201 | branches: 202 | ignore: /.*/ 203 | # only act on version tags 204 | tags: 205 | only: /^v[0-9]+(\.[0-9]+)*$/ 206 | 207 | - build-linux-amd64: 208 | requires: 209 | - setup 210 | filters: 211 | # ignore any commit on any branch by default 212 | branches: 213 | ignore: /.*/ 214 | # only act on version tags 215 | tags: 216 | only: /^v[0-9]+(\.[0-9]+)*$/ 217 | 218 | 219 | - deploy: 220 | requires: 221 | - build-linux-arm64 222 | - build-linux-amd64 223 | - build-win-amd64 224 | - build-darwin-amd64 225 | 226 | filters: 227 | # ignore any commit on any branch by default 228 | branches: 229 | ignore: /.*/ 230 | # only act on version tags 231 | tags: 232 | only: /^v[0-9]+(\.[0-9]+)*$/ 233 | -------------------------------------------------------------------------------- /saavn-cli.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import html 4 | import requests 5 | from sys import exit 6 | import sys 7 | import subprocess 8 | from time import sleep 9 | import base64 10 | 11 | # CONFIGURATION 12 | # API_URL - Saavn Unofficial API (@sumitkolhe) 13 | API_URL = "https://jiosaavn-api-privatecvc.vercel.app" 14 | # Search_EP - Endpoint for searching the API 15 | Search_EP = "/search/songs?query=" 16 | # 17 | Bitrate = 320 # Default Bitrate 18 | 19 | allowed_Bitrate = [12,48,96,160,320] 20 | # Bitrate Squence is important 21 | 22 | version = "1.2.0" # Client Version 23 | versionCode = 113 24 | Nullifier = " >/dev/null 2>&1 " # Nullifier to hide messy ffmpeg output 25 | debug="false" # Show additional output or not 26 | Bitrate_index = 4 #Default Bitrate Index of allowed_Bitrate 27 | # Initialization Text 28 | init_text = """ 29 | ___________________________________________ 30 | | Saavn-cli Command-Line Music Downloader | 31 | | ========== By Wiz64 ============ | 32 | | https://github.com/wiz64/saavn-cli | 33 | | | 34 | | For Help, run `saavn-cli help` | 35 | ------------------------------------------- 36 | """ 37 | #Help Text 38 | help_text = """ 39 | saavn-cli 40 | 41 | Supported Actions : 42 | # s / search 43 | Searching Tracks from the terminal 44 | Example - 45 | `saavn-cli s english 2022` 46 | 47 | # d / download ... 48 | NOT AVAIABLE IN THIS VERSION 49 | Downloading Saavn Songs from links or IDs 50 | Example - 51 | `saavn-cli d IEBQ7- DFEHNB- SJADKEi` 52 | 53 | To force bitrate, specify it after action like 54 | support values = 12,48,96,160,320 55 | `saavn-cli s:160 english songs` 56 | 57 | # h / help 58 | Shows this message 59 | 60 | # update 61 | Checks for updates 62 | 63 | ---- END ---- 64 | """ 65 | 66 | SearchTerm = "" 67 | DownloadItems = [] 68 | action = "" 69 | FetchedItems = [] 70 | work_dir = "/tmp/saavn-cli/" 71 | # on windows work_dir will point to C:\tmp\saavn-cli 72 | 73 | #incase ffmpeg not found 74 | FFMPEG_ERROR = "use your package manager - eg- sudo apt install ffmpeg" 75 | 76 | print("System Platform : "+sys.platform) 77 | if sys.platform == "win32": 78 | Nullifier = " >nul 2>&1" 79 | FFMPEG_ERROR = r"Download ffmpeg.exe and copy to C:\Windows\System32" 80 | if debug=="true": 81 | Nullifier = "" 82 | 83 | # Calling ffmpeg command to test if it is installed and working 84 | try: 85 | subprocess.run(['ffmpeg','-version'],check = True,stdout=subprocess.DEVNULL,stderr=subprocess.STDOUT) 86 | except: 87 | print(" [X] ERROR : ffmpeg command not found. Install ffmpeg..") 88 | print(" [0] "+FFMPEG_ERROR+" [0]") 89 | exit() 90 | 91 | def DoUpdate(version): 92 | #URL of Update service 93 | URL = ("https://raw.githubusercontent.com/wiz64/saavn-cli/main/latest-version") 94 | print(" GET "+URL) 95 | r = requests.get(URL) 96 | if r.status_code == 200: 97 | print("OK 200") 98 | Fetched = r.json() 99 | #print(Fetched) 100 | ServerVersion = Fetched['version'] 101 | ServerVersionCode = int(Fetched['versionCode']) 102 | # Compare ServerVersionCode with local versionCode 103 | if ServerVersionCode>versionCode: 104 | print("\n --- [OK] An Update is available, Download Here : "+Fetched['download']) 105 | exit() 106 | elif ServerVersionCode<=versionCode: 107 | print("\n\n Tool is currently at latest version. check back later") 108 | else: 109 | print(r.status_code) 110 | print("ERROR : Unable to reach Update API. Check Github") 111 | exit() 112 | 113 | ######## SEARCH FUNCTION 114 | def FetchSearch(search_term): 115 | URL = ( API_URL + Search_EP + str(search_term) + "&limit=60") 116 | # CALL THE SAAVN UNOFFICIAL API 117 | print(" GET "+URL) 118 | r = requests.get(URL) 119 | if(r.status_code == 200): 120 | print("OK 200") 121 | FetchedItems = r.json() 122 | i=0 #number counter 123 | for item in FetchedItems["data"]["results"][:20]: 124 | i=i+1 125 | song_id = str(item['id']) 126 | song_album = str(item['album']['name'])[:32] 127 | song_name = str(item['name'])[:32] 128 | if song_name == song_album:song_album=" " 129 | else: song_album = " @ "+song_album 130 | song_artist = str(item['primaryArtists'])[:32] 131 | print(f"({i}) ID: {song_id} | {song_name}{song_album} by {song_artist}") 132 | 133 | is_next_page=0 134 | NextPage_text ="" 135 | if(FetchedItems["data"]["results"][20:]): 136 | is_next_page = 1 137 | NextPage_text = " `n` for next page |" 138 | print(f" {NextPage_text} Enter Comma Sperated Numbers eg-(6,11,23) To Download | 0 or Enter to Cancel") 139 | # 140 | # # ASK FOR USER INPUT 141 | # 142 | song_indexes = input("Track No.s - ") 143 | 144 | #IF THERE ARE MORE THAN 20 RESULTS 145 | if (is_next_page==1 and str(song_indexes).lower() == 'n'): 146 | i=20 #number counter 147 | for item in FetchedItems["results"][20:]: 148 | i=i+1 149 | song_id = str(item['id']) 150 | song_album = html.unescape(str(item['album']['name'])[:32]) 151 | song_name = html.unescape(str(item['name'])[:32]) 152 | if song_name == song_album:song_album=" " 153 | else: song_album = " @ "+song_album 154 | song_artist = html.unescape(str(item['primaryArtists'])[:32]) 155 | print(f"({i}) ID: {song_id} | {song_name}{song_album} by {song_artist}") 156 | print(f"Enter Comma Sperated Numbers eg-(6,11,23) To Download | 0 or Enter to Cancel") 157 | song_indexes = input("Track No.s - ") 158 | 159 | 160 | 161 | if (song_indexes=="" or song_indexes == 0 or song_indexes=="0"): 162 | print("User Cancelled Download. Exiting") 163 | return 0 164 | song_indexes = song_indexes.split(",") 165 | print("Selected Download(s):") 166 | for song_index in song_indexes: 167 | i = int(song_index) 168 | item = FetchedItems['data']['results'][i-1] 169 | song_id = str(item['id']) 170 | song_album = str(item['album']['name']) 171 | song_name = str(item['name']) 172 | print(f"{i}th -> {song_name} - {song_album}") 173 | 174 | sleep(3) 175 | for song_index in song_indexes: 176 | i = int(song_index) 177 | item = FetchedItems['data']['results'][i-1] 178 | song_id = str(item['id']) 179 | song_album = html.unescape(str(item['album']['name'])).replace('"','') 180 | song_name = html.unescape(str(item['name'])).replace('"','') 181 | song_year = html.unescape(str(item['year'])).replace('"','') 182 | song_artist = html.unescape(str(item['primaryArtists'])).replace('"','') 183 | song_img_url = str(item['image'][2]['link']) 184 | song_copyright = html.unescape(str(item['copyright'])) 185 | song_publisher = "Saavn-cli" #str(item['publisher']) 186 | song_comment = "downloaded with https://github.com/wiz64/saavn-cli" 187 | song_download_url = str(item['downloadUrl'][Bitrate_index]['link']) 188 | 189 | 190 | print(f"Downloading ({i}) {song_name[:36] } @ { song_album[:36]} by {song_artist[:36]} with song_id {song_id} at {Bitrate}kbps at {work_dir}") 191 | if not os.path.isdir(work_dir): 192 | os.makedirs(work_dir) 193 | # Download Raw data to work_dir and compile later using ffmpeg 194 | song_data = requests.get(song_download_url) 195 | song_id = song_id.encode('ASCII') 196 | song_id = base64.b64encode(song_id).decode('ASCII') 197 | song_comment += " and song_id_base64="+song_id 198 | open(f'{work_dir}{song_id}_raw.mp3', 'wb').write(song_data.content) 199 | song_data = requests.get(song_img_url) 200 | open(f'{work_dir}{song_id}_raw.jpg', 'wb').write(song_data.content) 201 | output = os.getcwd()+f"/{song_name}-{song_year}.mp3" 202 | print("Compiling Metadata") 203 | #compile_command = f'cd "{work_dir}" && ls && ffmpeg -i "{song_id}_raw.mp3" -i "{song_id}_raw.jpg" -map 0:0 -map 1:0 -c copy -id3v2_version 3 -metadata title="{song_name}" -metadata album="{song_album}" -metadata artist="{song_artist[:72]}" -metadata date="{song_year}" -metadata album_artist="{song_artist[:72]}" -metadata copyright="{song_copyright}" -metadata publisher="{song_publisher}" -metadata comment="{song_comment}" -codec:a libmp3lame -b:a {Bitrate}k -hide_banner -y "{output}"{Nullifier} && #rm "{song_id}_raw.mp3" "{song_id}_raw.jpg"' 204 | compile_command = [ 205 | 'cd',work_dir, 206 | '&&','ffmpeg', 207 | '-i',song_id+"_raw.mp3", 208 | '-i',song_id+"_raw.jpg", 209 | '-map','0:0','-map','1:0', 210 | '-c','copy','-id3v2_version','3', 211 | '-metadata','title="'+song_name+'"', 212 | '-metadata','album="'+song_album+'"', 213 | '-metadata','artist="'+song_artist[:72]+'"', 214 | '-metadata','date="'+song_year+'"', 215 | '-metadata','album_artist="'+song_artist[:72]+'"', 216 | '-metadata','copyright="'+song_copyright+'"', 217 | '-metadata','publisher="'+song_publisher+'"', 218 | '-metadata','comment="'+song_comment+'"', 219 | '-codec:a','libmp3lame','-b:a',str(Bitrate)+'k','-hide_banner','-y','"'+output+'"', 220 | Nullifier, 221 | '&&','rm',song_id+"_raw.mp3"," ",song_id+"_raw.jpg" 222 | ] 223 | # join all in compile_command 224 | compile_command = ' '.join(compile_command) 225 | 226 | 227 | # COMPILING MP3 with Metadata and Cover Image 228 | print("========= STARTING COMPILATION ============") 229 | download_data =f""" 230 | TRACK ID : {song_id} 231 | NAME : {song_name[:32]} 232 | ARTIST : {song_artist[:42]} 233 | ALBUM : {song_album[:32]} 234 | PUBLISHING : {song_copyright} 235 | YEAR : {song_year} 236 | BITRATE : {Bitrate}kbps 237 | Output : `{output}` 238 | """ 239 | print(download_data) 240 | print("Executing FFMPEG... \n Compiling") 241 | try: 242 | subprocess.run(compile_command,check = True,stdout=subprocess.PIPE,stdin=subprocess.PIPE,stderr=subprocess.STDOUT,shell=True) 243 | except subprocess.CalledProcessError as e: 244 | print(e) 245 | print(" ERROR EXECUTING FFMPEG") 246 | print('======== COMPILATION FINISHED ========') 247 | else: 248 | print("ERROR : UNABLE TO FETCH FROM API") 249 | 250 | def ParseAction(argv): 251 | global Bitrate 252 | global Bitrate_index 253 | action="help" 254 | options = argv[1].split(":",1) 255 | if (options[0] == "s" or options[0] =="search"): 256 | action="search" 257 | print("Action : search") 258 | elif (options[0] =="exit"): 259 | action="exit" 260 | print("Action : exit") 261 | print(" Exiting....") 262 | exit() 263 | 264 | elif (options[0] == "d" or options[0] =="download"): 265 | action="download" 266 | print("Action : download | This action is currently under development. Check for Updates") 267 | elif(options[0]=="h" or options[0] =="help"): 268 | print(help_text) 269 | elif(options[0]=="update"): 270 | print("Checking for Updates") 271 | action="update" 272 | else: 273 | print("Unknown action : Check `saavn-cli help`") 274 | 275 | if(len(options)>1): 276 | if int(options[1]) in allowed_Bitrate: 277 | Bitrate = int(options[1]) 278 | Bitrate_index = allowed_Bitrate.index(Bitrate) 279 | print("Bitrate Selected : "+ str(Bitrate)) 280 | 281 | return action 282 | ## END CLI OPTIONS PARSING 283 | ##################### 284 | # START TERMS PARSING 285 | def ParseTerms(action,argv): 286 | terms = argv[2:] 287 | if(action=="search"): 288 | searchTerm = " ".join(terms) 289 | print("Search Terms : "+ searchTerm) 290 | return searchTerm 291 | if(action=="download"): 292 | terms = sys.argv[2:] 293 | DownloadItems = " ".join(terms).split(" ") 294 | for item in DownloadItems: 295 | print(item) 296 | return DownloadItems 297 | 298 | ####### END TERMS PARSING 299 | ####### DIRECT MODE FUNTCION 300 | def DirectMode(): 301 | while(1): 302 | Command = input(" Command : ") 303 | Command = Command.split(" ") 304 | if not Command[0] == "saavn-cli": 305 | Command.insert(0,"saavn-cli") 306 | if len(Command) > 1: 307 | action = ParseAction(Command) 308 | if len(Command) > 2: 309 | searchTerms = ParseTerms(action,Command) 310 | DoAction(action,searchTerms) 311 | 312 | ## 313 | def DoAction(action,Terms): 314 | if(action=="search"): 315 | try: 316 | FetchSearch(Terms) 317 | except: 318 | print("\n ERROR : Action Failed") 319 | exit() 320 | if(action=="update"): 321 | DoUpdate(version) 322 | 323 | 324 | print(init_text) 325 | 326 | # ARGUMENTS PARSING 327 | if(len(sys.argv) == 1): 328 | print("Running directly. Enter Command. eg- saavn-cli search new english songs") 329 | DirectMode() 330 | 331 | if (len(sys.argv) > 1): 332 | action = ParseAction(sys.argv) 333 | if (len(sys.argv) > 2): 334 | SearchTerm = ParseTerms(action,sys.argv) 335 | 336 | DoAction(action,SearchTerm) 337 | --------------------------------------------------------------------------------