├── .github └── workflows │ └── build.yaml ├── .gitignore ├── AUTHORS ├── Changelog.md ├── LICENSE.txt ├── README.md ├── ftransc ├── __init__.py ├── config │ ├── __init__.py │ ├── config.py │ └── data │ │ ├── presets.json │ │ └── tagmap.json ├── constants.py ├── core │ ├── __init__.py │ ├── queue │ │ ├── __init__.py │ │ └── consumer.py │ └── transcoders.py ├── errors.py ├── launcher.py ├── metadata │ ├── __init__.py │ └── metadata.py └── utils.py ├── setup.cfg ├── setup.py ├── static └── images │ ├── ftransc_cli.png │ ├── ftransc_gui.png │ ├── ftransc_gui_v4.1.4.png │ ├── ftransc_nautilus-scripts.png │ ├── rb_plugin0.png │ ├── rb_plugin1.png │ └── rb_plugin2.png └── tests ├── fixtures └── presets.json └── test_utils.py /.github/workflows/build.yaml: -------------------------------------------------------------------------------- 1 | name: build 2 | 3 | on: [push] 4 | 5 | jobs: 6 | lint: 7 | runs-on: ubuntu-latest 8 | strategy: 9 | matrix: 10 | python-version: [3.6, 3.7, 3.8, 3.9] 11 | fail-fast: true 12 | 13 | name: Lint on Python ${{ matrix.python-version }} 14 | 15 | steps: 16 | - uses: actions/checkout@v2 17 | 18 | - uses: actions/setup-python@v2 19 | with: 20 | python-version: ${{ matrix.python-version }} 21 | 22 | - name: Lint 23 | run: | 24 | pip install '.' 25 | pip install flake8 26 | flake8 . 27 | 28 | test: 29 | needs: [lint] 30 | runs-on: ubuntu-latest 31 | strategy: 32 | matrix: 33 | python-version: [ 3.6, 3.7, 3.8, 3.9] 34 | fail-fast: true 35 | 36 | name: Test on Python ${{ matrix.python-version }} 37 | 38 | steps: 39 | - uses: actions/checkout@v2 40 | 41 | - uses: actions/setup-python@v2 42 | with: 43 | python-version: ${{ matrix.python-version }} 44 | - name: Test 45 | run: | 46 | pip install '.' 47 | pip install nose 48 | nosetests --stop --verbose tests 49 | 50 | publish: 51 | needs: [test] 52 | name: Publish to PyPI 53 | runs-on: ubuntu-latest 54 | 55 | steps: 56 | - uses: actions/checkout@v2 57 | - uses: actions/setup-python@v2 58 | with: 59 | python-version: 3.9 60 | - name: Build package 61 | if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags') 62 | run: | 63 | pip install setuptools wheel 64 | python setup.py sdist 65 | - name: Publish package 66 | if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags') 67 | uses: pypa/gh-action-pypi-publish@master 68 | with: 69 | user: __token__ 70 | password: ${{ secrets.PYPI_TOKEN }} 71 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | dist 2 | *.egg-info 3 | *.pyc 4 | .idea 5 | *~ 6 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | Mkhanyisi Madlavana 2007 - 2009 2 | Mkhanyisi Madlavana 2010 - 2019 3 | Mkhanyisi Madlavana 2020 - present 4 | -------------------------------------------------------------------------------- /Changelog.md: -------------------------------------------------------------------------------- 1 | #### 7.0.2 - 30 Dec 2020 2 | - added --force-root option to force running as root 3 | 4 | #### 7.0.0 - XX XXX 20XX 5 | - dropped python 2 support. only supporting python 3.6 and higher 6 | 7 | #### 6.0.9 - 07 April 2017 8 | - Performance improvements when transcoding files from youtube playlists 9 | 10 | #### 6.0.6 - 05 Apr 2017 11 | - Adding youtube playlist support 12 | 13 | #### 6.0.0 - 01 Aug 2016 14 | - New reworked version. Not backward compatible. 15 | 16 | #### 5.1.7 - 26 Aug 2015 17 | - fixed rhythmbox plugin 18 | 19 | #### 5.1.6 - 10 Aug 2015 20 | - installing dependencies via make install 21 | 22 | #### 5.1.5 - 10 Aug 2015 23 | - added title and artist columns on ftransc_qt 24 | 25 | #### 5.1.4 - 10 Aug 2015 26 | - code cleanup. 27 | 28 | #### 5.1.3 - 09 Aug 2015 29 | - fixed working out ffmpeg/avconv utility 30 | 31 | #### 5.1.1 - 19 Nov 2013 32 | - fixed broken nautilus scripts 33 | - added convert to avi video (experimental) 34 | 35 | #### 5.1.0 - 17 Feb 2013 36 | - added the -x option to be able to use external encoder if it is 37 | available (eg using FFmpeg to decode and pipe to lame to encode instead of 38 | using ffmpeg to decode and encode via libmp3lame) 39 | 40 | #### 5.0.9 - 02 Feb 2013 41 | - added WavPack support 42 | 43 | #### 5.0.8 - 13 Jan 2013 44 | - using FFmpeg to encode any supported format where possible 45 | 46 | #### 5.0.7 - 04 Nov 2012 47 | - Added cover art insertion support to flac files 48 | 49 | #### 5.0.6 - 03 Nov 2012 50 | - Fixed bug when disk number is present in the metadata which caused all 51 | metadata to be dropped 52 | - Added support for extraction of metadata from .3gp files 53 | 54 | #### 5.0.5 - 28 Oct 2012 55 | - Merged the CoverTag class into MetaTag class 56 | - Fixed bug when the default 'normal' preset is not in the presets file 57 | - Generating fewer consumer processes when there are fewer files than CPU 58 | count 59 | 60 | #### 5.0.4 - 19 Aug 2012 61 | - added --list-formats option 62 | - added --processes/-p option 63 | 64 | #### 5.0.3 - 23 Jul 2012 65 | - moved away from ffmpeg to avconv 66 | 67 | #### 5.0.2 - 15 Jul 2012 68 | - added rhythmbox plugin 69 | - added CD ripping via cdparanoia 70 | 71 | #### 5.0.1 - 15 Jul 2012 72 | - added multithreading. 73 | 74 | #### 4.3.3 - 25 Mar 2012 75 | - fixed fatal KeyError when converting .aac and .mp4 files 76 | 77 | #### 4.3.2 - 10 Mar 2012 78 | - added album artist, disk number, and comment metadata tags to some 79 | formats 80 | - added output folder feature to fransc GUI, ftransc_qt 81 | 82 | #### 4.3.1 - 20 Feb 2012 83 | - added lyrics support for mp3, m4a and wma formats 84 | 85 | #### 4.3.0 - 20 Feb 2012 86 | - added the -o/--outdir option 87 | - fixed potential bug with --upgrade 88 | - refined parsing of playlist files 89 | 90 | #### 4.2.9 - 13 Jan 2012 91 | - added ability to convert audio files specified in PLS playlist file and 92 | XSPF playlist file via the --pls option and the --xspf option, resp. 93 | 94 | #### 4.2.8 - 10 Jan 2012 95 | - added ability to convert audio files specified in the M3U playlist file 96 | via the --m3u option 97 | 98 | #### 4.2.7 - 06 Jan 2012 99 | - added album art support when converting FROM ogg/flac TO mp3/m4a 100 | 101 | #### 4.2.6 - 05 Jan 2012 102 | - code cleanup 103 | - added album art support when converting from mp3 to m4a and vice versa 104 | 105 | #### 4.2.5 - 19 Dec 2011 106 | - code cleanup 107 | - proper message when --upgrade option is used while offline 108 | 109 | #### 4.2.4 - 19 Dec 2011 110 | - dropping checking mutagen version against version 1.17 since easy=True 111 | option of File() has been dropped. 112 | - fixed fatal errors when mutagen is not installed. instead, metadata 113 | support is dropped. 114 | 115 | #### 4.2.3 - 19 Dec 2011 116 | - code cleanup 117 | 118 | #### 4.2.2 - 19 Dec 2011 119 | - code cleanup 120 | 121 | #### 4.2.1 - 18 Dec 2011 122 | - removed the easy=True dependency on mutagen's File 123 | - fixed missing 'normal' ogg vorbis preset on configuration file 124 | 125 | #### 4.2.0 - 03 Dec 2011 126 | - removed quality presets from code and placed them in a separate file 127 | /etc/ftransc/presets. 128 | - added new option to specify a custom preset file: --presets 129 | 130 | #### 4.1.9 - 03 Dec 2011 131 | - reverted back metadata extraction method to earlier revision. 132 | 133 | #### 4.1.8 - 24 Nov 2011 134 | - Adding logging for executing as nautilus script 135 | 136 | #### 4.1.7 - 20 Nov 2011 137 | - Adding the options: -s/--silent, -l/--log, --debug, --notify 138 | - Fixed metadata issue with the year tag when converting from FLAC format. 139 | 140 | #### 4.1.6 - 20 Nov 2011 141 | - Adding mpc format support on ftransc nautilus scripts. 142 | 143 | #### 4.1.5 - 20 Nov 2011 144 | - extracting .wmv video metadata when extracting audio conntent from .wmv 145 | video. 146 | 147 | #### 4.1.4 - 20 Nov 2011 148 | - fixed issue of not dropping support to encode to some format when 149 | the necessary dependency is not installed. 150 | 151 | #### 4.1.3 - 13 Nov 2011 152 | - minor changes. 153 | 154 | #### 4.1.1 - 16 Oct 2011 155 | - added musepack audio format support. 156 | - added the -n / --no-tags option. 157 | 158 | #### 4.1.0 - 16 Oct 2011 159 | - added ftransc GUI, 'ftransc_qt'. 160 | - Explicitly drop support for python-mutagen versions older than 1.17. 161 | 162 | #### 4.0.6 - 30 Sep 2011 163 | - now using os.path.splitext() instead of a custom built function. 164 | - printing summary statistics of time taken to encode all files and 165 | average time taken per file. 166 | - added 'nautilus scripts' functionality. 167 | 168 | #### 4.0.5 - 25 Sep 2011 169 | - added the --upgrade option. 170 | 171 | #### 4.0.4 - 25 Sep 2011 172 | - added notify-send summary notification 173 | - fixed the issue with left over temporary .wav files when converting to 174 | FLAC format. 175 | 176 | #### 4.0.3 - 17 Apr 2011 177 | - no need to 'cd' to folder first before running ftransc. 178 | - /tmp/ftransc.log is not created anymore. 179 | - order of input files is sorted 180 | - prints out duration of conversion in seconds 181 | 182 | #### 4.0.1 - 05 Apr 2011 183 | - disable support of functionality when its dependency is not installed. 184 | this prevents crashes like trying to convert with an unavailable encoder. 185 | 186 | #### 4.0.0 - 22 Mar 2011 187 | - Written in python, thus, 'ftransc' & 'fmetadata' merged. 188 | - added the 'extreme' quality preset. 189 | - added directory walking. 190 | - transcode via wav option removed. 191 | - added a feature to unlock prelocked files. 192 | 193 | #### 3.2.3 - 03 Jan 2011 # unreleased 194 | - FLAC support: encode + decode + extract metadata + add metadata 195 | - polished logging. 196 | - polished code style 197 | - added the 'tiny' quality preset. 198 | 199 | #### 3.2.2 - 24 Dec 2010 # now available at code.google.com/p/ftransc 200 | - fmetadata python script. 201 | - decode to wav 202 | - trascode via wav and direct transcode 203 | - extract audio content from video files 204 | - added uninstallation option. 205 | - removed asftags, asfedit and aactags 206 | - removed dependency on mid3v2, id3 and vorbiscomment 207 | 208 | #### 3.2.1 - 22 Aug 2010 209 | - Support for non-ASCII Unicode characters via UTF-8. 210 | - Installation via a Makefile. 211 | - Option to check dependencies. 212 | - Overwriting disabled by default, with option to enable. 213 | - If source and destination files are the same, do not proceed. 214 | 215 | #### 3.2 - 16 Feb 2010 216 | - corrected man page installation path. 217 | 218 | #### 3.1 - 15 Jan 2010 219 | - log file not created when usage is displayed. 220 | - usage modified. 221 | - print usage when no args are present. 222 | - progress status message modified to highlight file being encoded. 223 | 224 | #### 3.0 - 24 Oct 2009 225 | - using FFmpeg to decode any type of input audio file. 226 | 227 | #### 2.5 - 20 Oct 2009 228 | - Removed decoding to WAV format first because temporarily consumes 229 | a lot of space and is slower. 230 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 2, June 1991 3 | 4 | Copyright (C) 1989, 1991 Free Software Foundation, Inc., 5 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 6 | Everyone is permitted to copy and distribute verbatim copies 7 | of this license document, but changing it is not allowed. 8 | 9 | Preamble 10 | 11 | The licenses for most software are designed to take away your 12 | freedom to share and change it. By contrast, the GNU General Public 13 | License is intended to guarantee your freedom to share and change free 14 | software--to make sure the software is free for all its users. This 15 | General Public License applies to most of the Free Software 16 | Foundation's software and to any other program whose authors commit to 17 | using it. (Some other Free Software Foundation software is covered by 18 | the GNU Lesser General Public License instead.) You can apply it to 19 | your programs, too. 20 | 21 | When we speak of free software, we are referring to freedom, not 22 | price. Our General Public Licenses are designed to make sure that you 23 | have the freedom to distribute copies of free software (and charge for 24 | this service if you wish), that you receive source code or can get it 25 | if you want it, that you can change the software or use pieces of it 26 | in new free programs; and that you know you can do these things. 27 | 28 | To protect your rights, we need to make restrictions that forbid 29 | anyone to deny you these rights or to ask you to surrender the rights. 30 | These restrictions translate to certain responsibilities for you if you 31 | distribute copies of the software, or if you modify it. 32 | 33 | For example, if you distribute copies of such a program, whether 34 | gratis or for a fee, you must give the recipients all the rights that 35 | you have. You must make sure that they, too, receive or can get the 36 | source code. And you must show them these terms so they know their 37 | rights. 38 | 39 | We protect your rights with two steps: (1) copyright the software, and 40 | (2) offer you this license which gives you legal permission to copy, 41 | distribute and/or modify the software. 42 | 43 | Also, for each author's protection and ours, we want to make certain 44 | that everyone understands that there is no warranty for this free 45 | software. If the software is modified by someone else and passed on, we 46 | want its recipients to know that what they have is not the original, so 47 | that any problems introduced by others will not reflect on the original 48 | authors' reputations. 49 | 50 | Finally, any free program is threatened constantly by software 51 | patents. We wish to avoid the danger that redistributors of a free 52 | program will individually obtain patent licenses, in effect making the 53 | program proprietary. To prevent this, we have made it clear that any 54 | patent must be licensed for everyone's free use or not licensed at all. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | GNU GENERAL PUBLIC LICENSE 60 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 61 | 62 | 0. This License applies to any program or other work which contains 63 | a notice placed by the copyright holder saying it may be distributed 64 | under the terms of this General Public License. The "Program", below, 65 | refers to any such program or work, and a "work based on the Program" 66 | means either the Program or any derivative work under copyright law: 67 | that is to say, a work containing the Program or a portion of it, 68 | either verbatim or with modifications and/or translated into another 69 | language. (Hereinafter, translation is included without limitation in 70 | the term "modification".) Each licensee is addressed as "you". 71 | 72 | Activities other than copying, distribution and modification are not 73 | covered by this License; they are outside its scope. The act of 74 | running the Program is not restricted, and the output from the Program 75 | is covered only if its contents constitute a work based on the 76 | Program (independent of having been made by running the Program). 77 | Whether that is true depends on what the Program does. 78 | 79 | 1. You may copy and distribute verbatim copies of the Program's 80 | source code as you receive it, in any medium, provided that you 81 | conspicuously and appropriately publish on each copy an appropriate 82 | copyright notice and disclaimer of warranty; keep intact all the 83 | notices that refer to this License and to the absence of any warranty; 84 | and give any other recipients of the Program a copy of this License 85 | along with the Program. 86 | 87 | You may charge a fee for the physical act of transferring a copy, and 88 | you may at your option offer warranty protection in exchange for a fee. 89 | 90 | 2. You may modify your copy or copies of the Program or any portion 91 | of it, thus forming a work based on the Program, and copy and 92 | distribute such modifications or work under the terms of Section 1 93 | above, provided that you also meet all of these conditions: 94 | 95 | a) You must cause the modified files to carry prominent notices 96 | stating that you changed the files and the date of any change. 97 | 98 | b) You must cause any work that you distribute or publish, that in 99 | whole or in part contains or is derived from the Program or any 100 | part thereof, to be licensed as a whole at no charge to all third 101 | parties under the terms of this License. 102 | 103 | c) If the modified program normally reads commands interactively 104 | when run, you must cause it, when started running for such 105 | interactive use in the most ordinary way, to print or display an 106 | announcement including an appropriate copyright notice and a 107 | notice that there is no warranty (or else, saying that you provide 108 | a warranty) and that users may redistribute the program under 109 | these conditions, and telling the user how to view a copy of this 110 | License. (Exception: if the Program itself is interactive but 111 | does not normally print such an announcement, your work based on 112 | the Program is not required to print an announcement.) 113 | 114 | These requirements apply to the modified work as a whole. If 115 | identifiable sections of that work are not derived from the Program, 116 | and can be reasonably considered independent and separate works in 117 | themselves, then this License, and its terms, do not apply to those 118 | sections when you distribute them as separate works. But when you 119 | distribute the same sections as part of a whole which is a work based 120 | on the Program, the distribution of the whole must be on the terms of 121 | this License, whose permissions for other licensees extend to the 122 | entire whole, and thus to each and every part regardless of who wrote it. 123 | 124 | Thus, it is not the intent of this section to claim rights or contest 125 | your rights to work written entirely by you; rather, the intent is to 126 | exercise the right to control the distribution of derivative or 127 | collective works based on the Program. 128 | 129 | In addition, mere aggregation of another work not based on the Program 130 | with the Program (or with a work based on the Program) on a volume of 131 | a storage or distribution medium does not bring the other work under 132 | the scope of this License. 133 | 134 | 3. You may copy and distribute the Program (or a work based on it, 135 | under Section 2) in object code or executable form under the terms of 136 | Sections 1 and 2 above provided that you also do one of the following: 137 | 138 | a) Accompany it with the complete corresponding machine-readable 139 | source code, which must be distributed under the terms of Sections 140 | 1 and 2 above on a medium customarily used for software interchange; or, 141 | 142 | b) Accompany it with a written offer, valid for at least three 143 | years, to give any third party, for a charge no more than your 144 | cost of physically performing source distribution, a complete 145 | machine-readable copy of the corresponding source code, to be 146 | distributed under the terms of Sections 1 and 2 above on a medium 147 | customarily used for software interchange; or, 148 | 149 | c) Accompany it with the information you received as to the offer 150 | to distribute corresponding source code. (This alternative is 151 | allowed only for noncommercial distribution and only if you 152 | received the program in object code or executable form with such 153 | an offer, in accord with Subsection b above.) 154 | 155 | The source code for a work means the preferred form of the work for 156 | making modifications to it. For an executable work, complete source 157 | code means all the source code for all modules it contains, plus any 158 | associated interface definition files, plus the scripts used to 159 | control compilation and installation of the executable. However, as a 160 | special exception, the source code distributed need not include 161 | anything that is normally distributed (in either source or binary 162 | form) with the major components (compiler, kernel, and so on) of the 163 | operating system on which the executable runs, unless that component 164 | itself accompanies the executable. 165 | 166 | If distribution of executable or object code is made by offering 167 | access to copy from a designated place, then offering equivalent 168 | access to copy the source code from the same place counts as 169 | distribution of the source code, even though third parties are not 170 | compelled to copy the source along with the object code. 171 | 172 | 4. You may not copy, modify, sublicense, or distribute the Program 173 | except as expressly provided under this License. Any attempt 174 | otherwise to copy, modify, sublicense or distribute the Program is 175 | void, and will automatically terminate your rights under this License. 176 | However, parties who have received copies, or rights, from you under 177 | this License will not have their licenses terminated so long as such 178 | parties remain in full compliance. 179 | 180 | 5. You are not required to accept this License, since you have not 181 | signed it. However, nothing else grants you permission to modify or 182 | distribute the Program or its derivative works. These actions are 183 | prohibited by law if you do not accept this License. Therefore, by 184 | modifying or distributing the Program (or any work based on the 185 | Program), you indicate your acceptance of this License to do so, and 186 | all its terms and conditions for copying, distributing or modifying 187 | the Program or works based on it. 188 | 189 | 6. Each time you redistribute the Program (or any work based on the 190 | Program), the recipient automatically receives a license from the 191 | original licensor to copy, distribute or modify the Program subject to 192 | these terms and conditions. You may not impose any further 193 | restrictions on the recipients' exercise of the rights granted herein. 194 | You are not responsible for enforcing compliance by third parties to 195 | this License. 196 | 197 | 7. If, as a consequence of a court judgment or allegation of patent 198 | infringement or for any other reason (not limited to patent issues), 199 | conditions are imposed on you (whether by court order, agreement or 200 | otherwise) that contradict the conditions of this License, they do not 201 | excuse you from the conditions of this License. If you cannot 202 | distribute so as to satisfy simultaneously your obligations under this 203 | License and any other pertinent obligations, then as a consequence you 204 | may not distribute the Program at all. For example, if a patent 205 | license would not permit royalty-free redistribution of the Program by 206 | all those who receive copies directly or indirectly through you, then 207 | the only way you could satisfy both it and this License would be to 208 | refrain entirely from distribution of the Program. 209 | 210 | If any portion of this section is held invalid or unenforceable under 211 | any particular circumstance, the balance of the section is intended to 212 | apply and the section as a whole is intended to apply in other 213 | circumstances. 214 | 215 | It is not the purpose of this section to induce you to infringe any 216 | patents or other property right claims or to contest validity of any 217 | such claims; this section has the sole purpose of protecting the 218 | integrity of the free software distribution system, which is 219 | implemented by public license practices. Many people have made 220 | generous contributions to the wide range of software distributed 221 | through that system in reliance on consistent application of that 222 | system; it is up to the author/donor to decide if he or she is willing 223 | to distribute software through any other system and a licensee cannot 224 | impose that choice. 225 | 226 | This section is intended to make thoroughly clear what is believed to 227 | be a consequence of the rest of this License. 228 | 229 | 8. If the distribution and/or use of the Program is restricted in 230 | certain countries either by patents or by copyrighted interfaces, the 231 | original copyright holder who places the Program under this License 232 | may add an explicit geographical distribution limitation excluding 233 | those countries, so that distribution is permitted only in or among 234 | countries not thus excluded. In such case, this License incorporates 235 | the limitation as if written in the body of this License. 236 | 237 | 9. The Free Software Foundation may publish revised and/or new versions 238 | of the General Public License from time to time. Such new versions will 239 | be similar in spirit to the present version, but may differ in detail to 240 | address new problems or concerns. 241 | 242 | Each version is given a distinguishing version number. If the Program 243 | specifies a version number of this License which applies to it and "any 244 | later version", you have the option of following the terms and conditions 245 | either of that version or of any later version published by the Free 246 | Software Foundation. If the Program does not specify a version number of 247 | this License, you may choose any version ever published by the Free Software 248 | Foundation. 249 | 250 | 10. If you wish to incorporate parts of the Program into other free 251 | programs whose distribution conditions are different, write to the author 252 | to ask for permission. For software which is copyrighted by the Free 253 | Software Foundation, write to the Free Software Foundation; we sometimes 254 | make exceptions for this. Our decision will be guided by the two goals 255 | of preserving the free status of all derivatives of our free software and 256 | of promoting the sharing and reuse of software generally. 257 | 258 | NO WARRANTY 259 | 260 | 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY 261 | FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN 262 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES 263 | PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED 264 | OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 265 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS 266 | TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE 267 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, 268 | REPAIR OR CORRECTION. 269 | 270 | 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 271 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR 272 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, 273 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING 274 | OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED 275 | TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY 276 | YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER 277 | PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE 278 | POSSIBILITY OF SUCH DAMAGES. 279 | 280 | END OF TERMS AND CONDITIONS 281 | 282 | How to Apply These Terms to Your New Programs 283 | 284 | If you develop a new program, and you want it to be of the greatest 285 | possible use to the public, the best way to achieve this is to make it 286 | free software which everyone can redistribute and change under these terms. 287 | 288 | To do so, attach the following notices to the program. It is safest 289 | to attach them to the start of each source file to most effectively 290 | convey the exclusion of warranty; and each file should have at least 291 | the "copyright" line and a pointer to where the full notice is found. 292 | 293 | 294 | Copyright (C) 295 | 296 | This program is free software; you can redistribute it and/or modify 297 | it under the terms of the GNU General Public License as published by 298 | the Free Software Foundation; either version 2 of the License, or 299 | (at your option) any later version. 300 | 301 | This program is distributed in the hope that it will be useful, 302 | but WITHOUT ANY WARRANTY; without even the implied warranty of 303 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 304 | GNU General Public License for more details. 305 | 306 | You should have received a copy of the GNU General Public License along 307 | with this program; if not, write to the Free Software Foundation, Inc., 308 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 309 | 310 | Also add information on how to contact you by electronic and paper mail. 311 | 312 | If the program is interactive, make it output a short notice like this 313 | when it starts in an interactive mode: 314 | 315 | Gnomovision version 69, Copyright (C) year name of author 316 | Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 317 | This is free software, and you are welcome to redistribute it 318 | under certain conditions; type `show c' for details. 319 | 320 | The hypothetical commands `show w' and `show c' should show the appropriate 321 | parts of the General Public License. Of course, the commands you use may 322 | be called something other than `show w' and `show c'; they could even be 323 | mouse-clicks or menu items--whatever suits your program. 324 | 325 | You should also get your employer (if you work as a programmer) or your 326 | school, if any, to sign a "copyright disclaimer" for the program, if 327 | necessary. Here is a sample; alter the names: 328 | 329 | Yoyodyne, Inc., hereby disclaims all copyright interest in the program 330 | `Gnomovision' (which makes passes at compilers) written by James Hacker. 331 | 332 | , 1 April 1989 333 | Ty Coon, President of Vice 334 | 335 | This General Public License does not permit incorporating your program into 336 | proprietary programs. If your program is a subroutine library, you may 337 | consider it more useful to permit linking proprietary applications with the 338 | library. If this is what you want to do, use the GNU Lesser General 339 | Public License instead of this License. 340 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://github.com/dopstar/ftransc/workflows/build/badge.svg?branch=master)](https://github.com/dopstar/ftransc/actions?query=workflow%3Abuild) 2 | [![Python Version](https://img.shields.io/pypi/pyversions/ftransc.svg)](https://pypi.python.org/pypi/ftransc) 3 | [![PyPI Status](https://img.shields.io/pypi/v/ftransc.svg)](https://pypi.python.org/pypi/ftransc) 4 | [![Downloads](https://img.shields.io/pypi/dm/ftransc.svg)](https://pypi.python.org/pypi/ftransc) 5 | [![Licence](https://img.shields.io/github/license/dopstar/ftransc.svg)](https://raw.githubusercontent.com/dopstar/ftransc/master/LICENSE.txt) 6 | # What is ftransc 7 | 8 | `ftransc` is the python audio conversion library. It can convert local files or files from youtube (even youtube playlists). 9 | 10 | 11 | ## Installing ftransc 12 | 13 | 14 | ftransc can be installed as follows: 15 | 16 | ``` 17 | pip install ftransc 18 | ``` 19 | 20 | Then FFMpeg must also installed as follows: 21 | 22 | - On Linux based systems: 23 | ``` 24 | sudo apt-get install ffmpeg lame flac vorbis-tools 25 | ``` 26 | 27 | - On Mac OS X: 28 | ``` 29 | brew install ffmpeg lame 30 | ``` 31 | 32 | 33 | 34 | ## Examples 35 | 36 | Example 1 - converting from MP3 to OGG: 37 | ``` 38 | ftransc -f ogg filename.mp3 39 | ``` 40 | The output file name for the above example will be 'filename.ogg' 41 | 42 | Example 2 - converting from MP3 to AAC, removing original file on success, using high quality preset: 43 | ``` 44 | ftransc -r -q extreme -f aac filename.mp3 45 | ``` 46 | 47 | Example 3 - extract audio content from a video files into the MP3 format, use best quality preset: 48 | ``` 49 | ftransc -q insane -f mp3 filename2.avi filename3.mpg filename4.vob ... 50 | ``` 51 | 52 | Example 4 - convert all audio files inside a given folder into WMA format. (This option is not recursive to child-folders) 53 | ``` 54 | ftransc -f wma --directory /path/to/folder_name 55 | ``` 56 | 57 | Example 5 - convert all audio audio files (and extract all audio content from video files) inside a given folder recursively including all sub-/child-folders, ftransc should be used with the 'find' command in the pipeline as follows: 58 | ``` 59 | find /path/to/folder_name -type f -print0 | xargs -0 ftransc -f aac -q high 60 | ``` 61 | 62 | ## ftransc Quality Presets 63 | 64 | ftransc uses quality presets called 'insane', 'extreme', 'high', 'normal', 'low', and 'tiny'. These presets are specified by the '-q' or '--quality' option of ftransc and are defined in the '/etc/ftransc/presets.conf' configuration file. 65 | 66 | The `/etc/ftransc/presets.conf` presets file can be overriden by use of the --presets option and specify the custom presets file to use or, if you know what you are doing, make changes directly on the it. 67 | 68 | 69 | ## ftransc Metadata Tags 70 | 71 | The following is the list of supported tags across audio formats that ftransc can encode to. N means the tag is not supported and hence is lost during conversion. Y means the tag is supported and is present on the new file after conversion: 72 | 73 | | *tag* | *m4a* | *mp3* | *ogg* | *flac* | *wma* | *mpc* | *wav* | *wv* | 74 | |-------|-------|-------|-------|--------|-------|-------|-------|------| 75 | | title | Y | Y | Y | Y | Y | Y | N | Y | 76 | | artist | Y | Y | Y | Y | Y | Y | N | Y | 77 | | album | Y | Y | Y | Y | Y | Y | N | Y | 78 | | genre | Y | Y | Y | Y | Y | Y | N | Y | 79 | | date | Y | Y | Y | Y | Y | Y | N | Y | 80 | | tracknumber | Y | Y | Y | Y | Y | Y | N | Y | 81 | | composer | Y | Y | Y | Y | Y | Y | N | N | 82 | | publisher | N | Y | N | N | Y | N | N | N | 83 | | lyrics | Y | Y | N | N | Y | N | N | N | 84 | | album art | Y | Y | N | Y | N | N | N | N | 85 | | album artist | N | N | N | N | N | N | N | N | 86 | | comment | N | N | N | N | N | N | N | N | 87 | 88 | ______ 89 | 90 | ## Screenshots 91 | 92 | The image below shows `ftransc` command in action on Terminal as well as the ftransc manpage (`man ftransc`): 93 | 94 | ![ftranansc_cli](https://raw.githubusercontent.com/dopstar/ftransc/master/static/images/ftransc_cli.png) 95 | 96 | _____ 97 | 98 | ftransc GUI front-end, *ftransc_qt*: 99 | 100 | ![ftranansc_qt](https://raw.githubusercontent.com/dopstar/ftransc/master/static/images/ftransc_gui.png) 101 | 102 | _____ 103 | 104 | ftransc also uses Nautilus Scripts, so you can right click selection of files and convert like: 105 | 106 | ![nautilus scripts](https://raw.githubusercontent.com/dopstar/ftransc/master/static/images/ftransc_nautilus-scripts.png) 107 | 108 | _____ 109 | 110 | ### ftransc plugin for Rhythmbox media player: 111 | 112 | - The ftransc plugin for rhythmbox media player allows one to send files from Rhythmbox music player to ftransc for conversion. 113 | ![enabling plugin](https://raw.githubusercontent.com/dopstar/ftransc/master/static/images/rb_plugin0.png) 114 | 115 | ____ 116 | 117 | - Enabling the plugin: 118 | ![enabling plugin](https://raw.githubusercontent.com/dopstar/ftransc/master/static/images/rb_plugin1.png) 119 | 120 | ____ 121 | 122 | - Converting songs with ftransc from Rhythmbox 123 | ![using plugin](https://raw.githubusercontent.com/dopstar/ftransc/master/static/images/rb_plugin2.png) 124 | 125 | 126 | -------------------------------------------------------------------------------- /ftransc/__init__.py: -------------------------------------------------------------------------------- 1 | import pkg_resources 2 | 3 | try: 4 | __version__ = pkg_resources.get_distribution("ftransc").version 5 | except pkg_resources.DistributionNotFound: 6 | __version__ = "0.0.1" 7 | -------------------------------------------------------------------------------- /ftransc/config/__init__.py: -------------------------------------------------------------------------------- 1 | from ftransc.config.config import ( 2 | AudioPresets, 3 | TagMap, 4 | ) 5 | 6 | 7 | __all__ = [ 8 | 'AudioPresets', 9 | 'TagMap', 10 | ] 11 | -------------------------------------------------------------------------------- /ftransc/config/config.py: -------------------------------------------------------------------------------- 1 | import os 2 | import copy 3 | import json 4 | 5 | 6 | class BaseConfig(object): 7 | config_file = "" 8 | 9 | def __init__(self): 10 | self.__data = {} 11 | self.__parse() 12 | 13 | def __parse(self): 14 | if self.config_file: 15 | config_file = os.path.join( 16 | os.path.dirname(__file__), "data", self.config_file 17 | ) 18 | with open(config_file) as fd: 19 | self.__data = json.load(fd) 20 | 21 | def __getattr__(self, item): 22 | try: 23 | return self.__data[item] 24 | except KeyError: 25 | raise AttributeError(item) 26 | 27 | def get(self, key, default=None): 28 | return self.__data.get(key, default) 29 | 30 | def as_dict(self): 31 | return copy.deepcopy(self.__data) 32 | 33 | 34 | class AudioPresets(BaseConfig): 35 | config_file = "presets.json" 36 | 37 | 38 | class TagMap(BaseConfig): 39 | config_file = "tagmap.json" 40 | -------------------------------------------------------------------------------- /ftransc/config/data/presets.json: -------------------------------------------------------------------------------- 1 | { 2 | "ogg_int": { 3 | "insane" : "-vn -ac 2 -acodec libvorbis -q 10", 4 | "extreme": "-vn -ac 2 -acodec libvorbis -q 8", 5 | "high" : "-vn -ac 2 -acodec libvorbis -q 6", 6 | "normal" : "-vn -ac 2 -acodec libvorbis -q 3", 7 | "low" : "-vn -ac 2 -acodec libvorbis -q 1", 8 | "tiny" : "-vn -ac 1 -acodec libvorbis -q 0 -ar 32000" 9 | }, 10 | "mp3_int": { 11 | "insane" : "-vn -ac 2 -acodec libmp3lame -ab 320k", 12 | "extreme": "-vn -ac 2 -acodec libmp3lame -aq 0", 13 | "high" : "-vn -ac 2 -acodec libmp3lame -aq 2", 14 | "normal" : "-vn -ac 2 -acodec libmp3lame -aq 4", 15 | "low" : "-vn -ac 2 -acodec libmp3lame -aq 9", 16 | "tiny" : "-vn -ac 1 -acodec libmp3lame -ab 32k -ar 32000" 17 | }, 18 | "m4a_int": { 19 | "insane" : "-vn -ac 2 -acodec libfaac -aq 250", 20 | "extreme": "-vn -ac 2 -acodec libfaac -aq 220", 21 | "high" : "-vn -ac 2 -acodec libfaac -aq 170", 22 | "normal" : "-vn -ac 2 -acodec libfaac -aq 100", 23 | "low" : "-vn -ac 2 -acodec libfaac -aq 50", 24 | "tiny" : "-vn -ac 1 -acodec libfaac -aq 30 -ar 32000" 25 | }, 26 | "wma_int": { 27 | "insane" : "-vn -ac 2 -acodec wmav2 -ab 320k", 28 | "extreme": "-vn -ac 2 -acodec wmav2 -ab 256k", 29 | "high" : "-vn -ac 2 -acodec wmav2 -ab 192k", 30 | "normal" : "-vn -ac 2 -acodec wmav2 -ab 128k", 31 | "low" : "-vn -ac 2 -acodec wmav2 -ab 64k", 32 | "tiny" : "-vn -ac 1 -acodec wmav2 -ab 32k -ar 32000" 33 | }, 34 | "flac_int": { 35 | "insane" : "-vn -acodec flac -compression_level 10", 36 | "extreme": "-vn -acodec flac -compression_level 8", 37 | "high" : "-vn -acodec flac -compression_level 7", 38 | "normal" : "-vn -acodec flac -compression_level 5", 39 | "low" : "-vn -acodec flac -compression_level 3", 40 | "tiny" : "-vn -acodec flac -compression_level 1" 41 | }, 42 | "avi_int": { 43 | "insane" : "-vcodec mpeg4 -vtag divx -vb 2000k -acodec libmp3lame -ac 2 -ab 128k", 44 | "extreme": "-vcodec mpeg4 -vtag divx -vb 1700k -acodec libmp3lame -ac 2 -ab 128k", 45 | "high" : "-vcodec mpeg4 -vtag divx -vb 1400k -acodec libmp3lame -ac 2 -ab 128k", 46 | "normal" : "-vcodec mpeg4 -vtag divx -vb 1300k -acodec libmp3lame -ac 2 -ab 128k", 47 | "low" : "-vcodec mpeg4 -vtag divx -vb 900k -acodec libmp3lame -ac 2 -ab 128k", 48 | "tiny" : "-vcodec mpeg4 -vtag divx -vb 600k -acodec libmp3lame -ac 2 -ab 128k" 49 | }, 50 | "mpc_ext": { 51 | "insane" : "--overwrite --braindead", 52 | "extreme": "--overwrite -- insane", 53 | "high" : "--overwrite --extreme", 54 | "normal" : "--overwrite --normal", 55 | "low" : "--overwrite --telephone", 56 | "tiny" : "--overwrite --quality 1.00" 57 | }, 58 | "wv_ext": { 59 | "insane" : "-i -y -hh", 60 | "extreme": "-i -y -hh", 61 | "high" : "-i -y -h", 62 | "normal" : "-i -y", 63 | "low" : "-i -y -f", 64 | "tiny" : "-i -y -f" 65 | }, 66 | "ogg_ext": { 67 | "insane" : "-q 10", 68 | "extreme": "-q 8", 69 | "high" : "-q 6", 70 | "normal" : "-q 3", 71 | "low" : "-q 1", 72 | "tiny" : "-q -1" 73 | }, 74 | "mp3_ext": { 75 | "insane" : "--cbr -b 320", 76 | "extreme": "-V 0", 77 | "high" : "-V 2", 78 | "normal" : "-V 4", 79 | "low" : "--preset 64", 80 | "tiny" : "--cbr -b 32 -m m -s 32" 81 | }, 82 | "flac_ext": { 83 | "insane" : "--best", 84 | "extreme": "-7", 85 | "high" : "-6", 86 | "normal" : "-5", 87 | "low" : "-3", 88 | "tiny" : "--fast" 89 | }, 90 | "m4a_ext": { 91 | "insane" : "-w -q 500", 92 | "extreme": "-w -q 350", 93 | "high" : "-w -q 200", 94 | "normal" : "-w", 95 | "low" : "-w -q 50", 96 | "tiny" : "-w -q 10" 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /ftransc/config/data/tagmap.json: -------------------------------------------------------------------------------- 1 | { 2 | ".3gp": { 3 | "album": "\u00a9alb", 4 | "albumart": "covr", 5 | "albumartist": "aART", 6 | "artist": "\u00a9ART", 7 | "comment": "\u00a9cmt", 8 | "composer": "\u00a9wrt", 9 | "disk": "disk", 10 | "genre": "\u00a9gen", 11 | "lyrics": "\u00a9lyr", 12 | "title": "\u00a9nam", 13 | "tracknumber": "trkn", 14 | "year": "\u00a9day" 15 | }, 16 | ".aac": { 17 | "album": "\u00a9alb", 18 | "albumartist": "aART", 19 | "artist": "\u00a9ART", 20 | "comment": "\u00a9cmt", 21 | "composer": "\u00a9wrt", 22 | "disk": "disk", 23 | "genre": "\u00a9gen", 24 | "lyrics": "\u00a9lyr", 25 | "title": "\u00a9nam", 26 | "tracknumber": "trkn", 27 | "year": "\u00a9day" 28 | }, 29 | ".flac": { 30 | "album": "album", 31 | "albumart": "metadata_block_picture", 32 | "artist": "artist", 33 | "composer": "composer", 34 | "genre": "genre", 35 | "title": "title", 36 | "tracknumber": "tracknumber", 37 | "year": "date" 38 | }, 39 | ".m4a": { 40 | "album": "\u00a9alb", 41 | "albumart": "covr", 42 | "albumartist": "aART", 43 | "artist": "\u00a9ART", 44 | "comment": "\u00a9cmt", 45 | "composer": "\u00a9wrt", 46 | "disk": "disk", 47 | "genre": "\u00a9gen", 48 | "lyrics": "\u00a9lyr", 49 | "title": "\u00a9nam", 50 | "tracknumber": "trkn", 51 | "year": "\u00a9day" 52 | }, 53 | ".mp3": { 54 | "album": "TALB", 55 | "albumart": "APIC:", 56 | "artist": "TPE1", 57 | "comment": "COMM", 58 | "composer": "TCOM", 59 | "disk": "TPOS", 60 | "genre": "TCON", 61 | "lyrics": "USLT", 62 | "publisher": "TPUB", 63 | "title": "TIT2", 64 | "tracknumber": "TRCK", 65 | "year": "TDRC" 66 | }, 67 | ".mp4": { 68 | "album": "\u00a9alb", 69 | "albumart": "covr", 70 | "albumartist": "aART", 71 | "artist": "\u00a9ART", 72 | "comment": "\u00a9cmt", 73 | "composer": "\u00a9wrt", 74 | "disk": "disk", 75 | "genre": "\u00a9gen", 76 | "lyrics": "\u00a9lyr", 77 | "title": "\u00a9nam", 78 | "tracknumber": "trkn", 79 | "year": "\u00a9day" 80 | }, 81 | ".mpc": { 82 | "album": "Album", 83 | "artist": "Artist", 84 | "composer": "Composer", 85 | "genre": "Genre", 86 | "title": "Title", 87 | "tracknumber": "Track", 88 | "year": "Year" 89 | }, 90 | ".ogg": { 91 | "album": "album", 92 | "albumart": "metadata_block_picture", 93 | "artist": "artist", 94 | "composer": "composer", 95 | "genre": "genre", 96 | "title": "title", 97 | "tracknumber": "tracknumber", 98 | "year": "date" 99 | }, 100 | ".wma": { 101 | "album": "WM/AlbumTitle", 102 | "albumartist": "WM/AlbumArtist", 103 | "artist": "Author", 104 | "composer": "WM/Composer", 105 | "genre": "WM/Genre", 106 | "lyrics": "WM/Lyrics", 107 | "publisher": "WM/Publisher", 108 | "title": "Title", 109 | "tracknumber": "WM/TrackNumber", 110 | "year": "WM/Year" 111 | }, 112 | ".wmv": { 113 | "album": "WM/AlbumTitle", 114 | "artist": "Author", 115 | "composer": "WM/Composer", 116 | "genre": "WM/Genre", 117 | "lyrics": "WM/Lyrics", 118 | "publisher": "WM/Publisher", 119 | "title": "Title", 120 | "tracknumber": "WM/TrackNumber", 121 | "year": "WM/Year" 122 | }, 123 | ".wv": { 124 | "album": "album", 125 | "artist": "artist", 126 | "genre": "genre", 127 | "title": "title", 128 | "tracknumber": "track", 129 | "year": "year" 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /ftransc/constants.py: -------------------------------------------------------------------------------- 1 | import shutil 2 | 3 | 4 | def determine_ffmpeg_utility(): 5 | for util in ["avconv", "ffmpeg"]: 6 | if shutil.which(util) is not None: 7 | return util 8 | raise SystemExit("ffmpeg/avconv not installed") 9 | 10 | 11 | LOGFILE = "/dev/null" 12 | SUPPORTED_FORMATS = {"mp3", "wma", "wav", "ogg", "flac", "m4a", "mpc", "wv", "avi"} 13 | EXTERNAL_FORMATS = {"mpc", "wv"} 14 | INTERNAL_FORMATS = SUPPORTED_FORMATS - EXTERNAL_FORMATS 15 | EXTERNAL_ENCODERS = { 16 | "mpc": "mppenc", 17 | "wv": "wavpack", 18 | "mp3": "lame", 19 | "ogg": "oggenc", 20 | "m4a": "faac", 21 | "flac": "flac", 22 | } 23 | EXTERNAL_ENCODER_OUTPUT_OPT = { 24 | "mppenc": "", 25 | "lame": "", 26 | "faac": "-o", 27 | "flac": "-o", 28 | "oggenc": "-o", 29 | "wavpack": "-o", 30 | } 31 | FFMPEG_AVCONV = determine_ffmpeg_utility() 32 | DEPENDENCIES = { 33 | "cdparanoia": [], 34 | FFMPEG_AVCONV: list(SUPPORTED_FORMATS), 35 | } 36 | for audio_format, encoder in EXTERNAL_ENCODERS.items(): 37 | DEPENDENCIES[encoder] = [audio_format] 38 | -------------------------------------------------------------------------------- /ftransc/core/__init__.py: -------------------------------------------------------------------------------- 1 | from ftransc.core.transcoders import ( 2 | transcode, 3 | download_from_youtube, 4 | download_from_youtube_playlist, 5 | ) 6 | 7 | 8 | __all__ = [ 9 | 'transcode', 10 | 'download_from_youtube', 11 | 'download_from_youtube_playlist', 12 | ] 13 | -------------------------------------------------------------------------------- /ftransc/core/queue/__init__.py: -------------------------------------------------------------------------------- 1 | from ftransc.core.queue.consumer import worker 2 | 3 | 4 | __all__ = [ 5 | 'worker', 6 | ] 7 | -------------------------------------------------------------------------------- /ftransc/core/queue/consumer.py: -------------------------------------------------------------------------------- 1 | import os 2 | import time 3 | 4 | try: 5 | import Queue as queue 6 | except ImportError: 7 | import queue 8 | import logging 9 | 10 | import blessings 11 | 12 | from ftransc.metadata import Metadata 13 | from ftransc.core import ( 14 | transcode, 15 | download_from_youtube, 16 | download_from_youtube_playlist, 17 | ) 18 | import ftransc.utils 19 | 20 | 21 | term = blessings.Terminal() 22 | logger = logging.getLogger(__name__) 23 | 24 | 25 | def worker( 26 | input_q, 27 | cpu_count, 28 | home_directory, 29 | output_directory, 30 | audio_format, 31 | audio_preset, 32 | options, 33 | is_delayed_quit, 34 | ): 35 | exit_delay = 0 36 | if is_delayed_quit: 37 | exit_delay = 10 38 | while True: 39 | if input_q.empty(): 40 | if exit_delay: 41 | time.sleep(3) 42 | exit_delay -= 1 43 | continue 44 | logger.info(term.bold("Shutting down worker: %s"), cpu_count) 45 | break 46 | try: 47 | filename = input_q.get(False) 48 | except queue.Empty as err: 49 | logger.debug("%s: %s", type(err), str(err)) 50 | continue 51 | 52 | files = [filename] 53 | if ftransc.utils.is_url(filename): 54 | if ftransc.utils.is_youtube_playlist(filename): 55 | download_from_youtube_playlist(filename, output_stream_queue=input_q) 56 | exit_delay = 0 57 | input_q.task_done() 58 | continue 59 | else: 60 | files = [download_from_youtube(filename)] 61 | 62 | try: 63 | __process_file( 64 | files, 65 | input_q, 66 | cpu_count, 67 | home_directory, 68 | output_directory, 69 | audio_format, 70 | audio_preset, 71 | options, 72 | ) 73 | except Exception as err: 74 | logger.error("{0}: {1}".format(err.__class__.__name__, str(err))) 75 | finally: 76 | input_q.task_done() 77 | 78 | 79 | def __process_file( 80 | files, 81 | input_q, 82 | cpu_count, 83 | home_directory, 84 | output_directory, 85 | audio_format, 86 | audio_preset, 87 | options, 88 | ): 89 | for filename in files: 90 | new_dir = os.path.dirname(filename) 91 | input_file_name = os.path.basename(filename) 92 | output_file_name = os.path.splitext(input_file_name)[0] + "." + audio_format 93 | if output_directory: 94 | output_file_name = output_directory + os.sep + output_file_name 95 | if not output_directory.endswith(os.sep): 96 | output_directory += os.sep 97 | if new_dir: 98 | if not os.path.isabs(new_dir): 99 | new_dir = home_directory + os.path.sep + new_dir 100 | 101 | new_dir = os.path.realpath(new_dir) 102 | if new_dir != os.getcwd(): 103 | os.chdir(new_dir) 104 | else: 105 | if os.getcwd() != home_directory and not os.walk: 106 | os.chdir(home_directory) 107 | 108 | if output_file_name == input_file_name: 109 | logger.warning( 110 | term.yellow( 111 | "[{0}] {1} [input = output][skipped]".format( 112 | cpu_count, input_file_name 113 | ) 114 | ) 115 | ) 116 | input_q.task_done() 117 | continue 118 | if not os.path.exists(input_file_name): 119 | logger.warning( 120 | term.yellow( 121 | "[{0}] {1} [does not exist]".format(cpu_count, input_file_name) 122 | ) 123 | ) 124 | input_q.task_done() 125 | continue 126 | if os.path.isfile(output_file_name) and not options.overwrite: 127 | logger.warning( 128 | term.yellow( 129 | '[{0}] {1} [use "-w" to overwrite][skipped]'.format( 130 | cpu_count, input_file_name 131 | ) 132 | ) 133 | ) 134 | input_q.task_done() 135 | continue 136 | if os.path.isdir(input_file_name) and options.walk is None: 137 | logger.warning( 138 | term.yellow( 139 | '[{0}] {1} [use "--directory"][skipped]'.format( 140 | cpu_count, input_file_name 141 | ) 142 | ) 143 | ) 144 | input_q.task_done() 145 | continue 146 | 147 | swp_file = ".%s.swp" % input_file_name 148 | if os.path.isfile(swp_file) and not options.unlock: 149 | logger.warning( 150 | term.yellow( 151 | '[{0}] {1} [use "-u" to unlock][skipped]'.format( 152 | cpu_count, input_file_name 153 | ) 154 | ) 155 | ) 156 | input_q.task_done() 157 | continue 158 | elif not os.path.isfile(swp_file): 159 | try: 160 | with open(swp_file, "w"): 161 | pass 162 | except IOError as err: 163 | input_q.task_done() 164 | logger.fatal( 165 | term.red( 166 | "[{0}] No permissions to write to this folder".format(cpu_count) 167 | ) 168 | ) 169 | raise SystemExit(str(err)) 170 | 171 | try: 172 | metadata = Metadata(input_file_name) 173 | except IOError: 174 | logger.warning( 175 | term.yellow("[{0}][{1}] Unreadable".format(cpu_count, input_file_name)) 176 | ) 177 | os.remove(swp_file) 178 | input_q.task_done() 179 | continue 180 | 181 | if transcode( 182 | input_file_name, 183 | audio_format, 184 | output_directory, 185 | audio_preset, 186 | options.external_encoder, 187 | ): 188 | logger.info( 189 | term.green( 190 | "[{0}][to {1}] {2} [Success]".format( 191 | cpu_count, audio_format.upper(), input_file_name 192 | ) 193 | ) 194 | ) 195 | if options.remove: 196 | os.remove(input_file_name) 197 | os.remove(swp_file) 198 | else: 199 | logger.error( 200 | term.bold_red( 201 | "[{0}][to {1}] {2} [Fail]".format( 202 | cpu_count, audio_format.upper(), input_file_name 203 | ) 204 | ) 205 | ) 206 | os.remove(swp_file) 207 | input_q.task_done() 208 | continue 209 | 210 | metadata.insert_tags(output_file_name) 211 | -------------------------------------------------------------------------------- /ftransc/core/transcoders.py: -------------------------------------------------------------------------------- 1 | import os 2 | import logging 3 | import subprocess 4 | 5 | import pafy 6 | import blessings 7 | 8 | from ftransc.constants import ( 9 | EXTERNAL_FORMATS, 10 | EXTERNAL_ENCODERS, 11 | EXTERNAL_ENCODER_OUTPUT_OPT, 12 | FFMPEG_AVCONV, 13 | ) 14 | 15 | import ftransc.utils 16 | 17 | 18 | term = blessings.Terminal() 19 | logger = logging.getLogger(__name__) 20 | 21 | 22 | def transcode( 23 | input_file_name, 24 | output_audio_format, 25 | output_folder="./", 26 | audio_preset=None, 27 | external_encoder=False, 28 | ): 29 | output_folder = output_folder or "./" 30 | audio_preset = audio_preset or "" 31 | output_audio_format = output_audio_format.lower() 32 | base_input_file_name, input_ext = os.path.splitext(input_file_name) 33 | output_file_name = ( 34 | output_folder 35 | + "/" 36 | + os.path.basename(base_input_file_name) 37 | + "." 38 | + output_audio_format 39 | ) 40 | 41 | encoder = _get_external_encoder(output_audio_format) 42 | cmdline = [FFMPEG_AVCONV, "-y", "-i", input_file_name] 43 | 44 | if encoder and (external_encoder or output_audio_format in EXTERNAL_FORMATS): 45 | output_opt = EXTERNAL_ENCODER_OUTPUT_OPT.get(encoder, "") 46 | cmdline += ["-f", "wav", "/dev/stdout"] 47 | cmdline2 = [encoder] + audio_preset.split() + ["-"] 48 | if output_opt: 49 | cmdline2 += [output_opt] 50 | cmdline2 += [output_file_name] 51 | 52 | logger.debug( 53 | "Command-Line: `{term.green}{0} | {1}{term.normal}`".format( 54 | " ".join(cmdline), " ".join(cmdline2), term=term 55 | ) 56 | ) 57 | pipeline1 = subprocess.Popen( 58 | cmdline, stdout=subprocess.PIPE, stderr=subprocess.PIPE 59 | ) 60 | pipeline = subprocess.Popen( 61 | cmdline2, 62 | stdin=pipeline1.stdout, 63 | stdout=subprocess.PIPE, 64 | stderr=subprocess.STDOUT, 65 | ) 66 | else: 67 | cmdline += audio_preset.split() + [output_file_name] 68 | logger.debug( 69 | "Command-Line: `{term.green}{0}{term.normal}`".format( 70 | " ".join(cmdline), term=term 71 | ) 72 | ) 73 | pipeline = subprocess.Popen( 74 | cmdline, stdout=subprocess.PIPE, stderr=subprocess.STDOUT 75 | ) 76 | 77 | std_out, std_err = pipeline.communicate() 78 | if std_out: 79 | logger.debug(std_out) 80 | if std_err: 81 | logger.debug(std_err) 82 | return pipeline.returncode == 0 83 | 84 | 85 | def _get_external_encoder(audio_format): 86 | return EXTERNAL_ENCODERS.get(audio_format) 87 | 88 | 89 | def download_from_youtube(url): 90 | logger.debug("Fetching audio from [{0}]".format(url)) 91 | stream = pafy.new(url).getbestaudio() 92 | logger.debug("Found audio/video stream:\n{0}".format(stream)) 93 | filename = ftransc.utils.get_safe_filename(stream.title) 94 | return stream.download(filepath=filename, quiet=True) 95 | 96 | 97 | def download_from_youtube_playlist(playlist_url, output_stream_queue=None): 98 | playlist = pafy.get_playlist(playlist_url) 99 | files = [] 100 | if playlist and playlist.get("items"): 101 | cwd = os.getcwd() 102 | folder_name = os.path.join( 103 | cwd, ftransc.utils.get_safe_filename(playlist["title"]) 104 | ) 105 | os.mkdir(folder_name) 106 | os.chdir(folder_name) 107 | for item in playlist["items"]: 108 | try: 109 | stream = item["pafy"].getbestaudio() 110 | except KeyError: 111 | continue 112 | filename = ftransc.utils.get_safe_filename(stream.title) 113 | output_filename = os.path.join( 114 | folder_name, stream.download(filepath=filename, quiet=True) 115 | ) 116 | if output_stream_queue is not None: 117 | output_stream_queue.put(output_filename) 118 | else: 119 | files.append(output_filename) 120 | os.chdir(cwd) 121 | return files 122 | -------------------------------------------------------------------------------- /ftransc/errors.py: -------------------------------------------------------------------------------- 1 | class FtranscError(Exception): 2 | pass 3 | 4 | 5 | class AudioPresetError(FtranscError): 6 | pass 7 | -------------------------------------------------------------------------------- /ftransc/launcher.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import time 4 | import logging 5 | import multiprocessing 6 | import pathlib 7 | 8 | import ftransc.core.queue 9 | import ftransc.utils 10 | 11 | 12 | def cli(): 13 | opt, files = ftransc.utils.parse_args() 14 | 15 | if opt.silent: 16 | log_level = logging.CRITICAL 17 | elif opt.debug: 18 | log_level = logging.DEBUG 19 | else: 20 | log_level = logging.INFO 21 | log_format = "[%(levelname)s] %(message)s" 22 | logging.basicConfig(stream=sys.stdout, level=log_level, format=log_format) 23 | 24 | if "USER" in os.environ and os.environ["USER"] == "root" and not opt.force_root: 25 | raise SystemExit( 26 | "It is not safe to run ftransc as root. " 27 | "use '--force-root' if you know what you are doing" 28 | ) 29 | 30 | if not files and not opt.walk and not opt.cdrip: 31 | raise SystemExit("ftransc: no input file") 32 | 33 | files = sorted(list(set(files))) # remove duplicates 34 | home_directory = os.getcwd() 35 | audio_format = opt.format.lower() 36 | audio_quality = opt.quality.lower() 37 | audio_preset = ftransc.utils.get_audio_presets( 38 | audio_format, audio_quality=audio_quality, external_encoder=opt.external_encoder 39 | ) 40 | 41 | if opt.walk is not None: 42 | working_directory, files = ".", [] 43 | for w, _, f in os.walk(opt.walk): 44 | working_directory, files = w, f 45 | break 46 | os.chdir(working_directory) 47 | 48 | if opt.cdrip: 49 | files = ftransc.utils.rip_compact_disc() 50 | 51 | queue = multiprocessing.JoinableQueue() 52 | for filename in files: 53 | queue.put(filename) 54 | 55 | time.sleep(1) # wait a sec before start processing. queue might not be full yet 56 | num_workers = ftransc.utils.determine_number_of_workers(files, opt.num_procs) 57 | 58 | output_directory = "" 59 | if opt.outdir: 60 | path = pathlib.Path(opt.outdir) 61 | if not path.exists(): 62 | path.mkdir(parents=True, exist_ok=True) 63 | 64 | output_directory = str(path.expanduser()) 65 | for process_count in range(1, num_workers + 1): 66 | process_name = "P%d" % process_count 67 | exit_delay = ftransc.utils.has_youtube_playlist(files) 68 | worker_args = ( 69 | queue, 70 | process_name, 71 | home_directory, 72 | output_directory, 73 | audio_format, 74 | audio_preset, 75 | opt, 76 | exit_delay, 77 | ) 78 | process = multiprocessing.Process( 79 | target=ftransc.core.queue.worker, args=worker_args 80 | ) 81 | process.daemon = True 82 | process.start() 83 | 84 | queue.join() 85 | -------------------------------------------------------------------------------- /ftransc/metadata/__init__.py: -------------------------------------------------------------------------------- 1 | from ftransc.metadata.metadata import Metadata 2 | 3 | __all__ = [ 4 | 'Metadata', 5 | ] 6 | -------------------------------------------------------------------------------- /ftransc/metadata/metadata.py: -------------------------------------------------------------------------------- 1 | import os 2 | import base64 3 | 4 | import mutagen 5 | import mutagen.id3 6 | import mutagen.mp3 7 | import mutagen.mp4 8 | import mutagen.asf 9 | import mutagen.flac 10 | import mutagen.apev2 11 | import mutagen.musepack 12 | import mutagen.oggvorbis 13 | 14 | 15 | from ftransc.config import TagMap 16 | 17 | 18 | class Metadata(object): 19 | """ 20 | handles tag extraction and insertion into and/or from audio files 21 | """ 22 | 23 | __tag_mapping = {} 24 | 25 | __id3_mapping = { 26 | "artist": mutagen.id3.TPE1, 27 | "album": mutagen.id3.TALB, 28 | "title": mutagen.id3.TIT2, 29 | "genre": mutagen.id3.TCON, 30 | "year": mutagen.id3.TDRC, 31 | "tracknumber": mutagen.id3.TRCK, 32 | "composer": mutagen.id3.TCOM, 33 | "lyrics": mutagen.id3.USLT, 34 | "disk": mutagen.id3.TPOS, 35 | } 36 | __opener = { 37 | ".mp3": mutagen.mp3.Open, 38 | ".wma": mutagen.asf.Open, 39 | ".m4a": mutagen.mp4.Open, 40 | ".flac": mutagen.flac.Open, 41 | ".wv": mutagen.apev2.APEv2, 42 | ".mpc": mutagen.musepack.Open, 43 | ".ogg": mutagen.oggvorbis.Open, 44 | } 45 | 46 | def __init__(self, input_file): 47 | self.input_file = input_file 48 | self.input_tags = {key: None for key in self.__id3_mapping} 49 | self.input_tags["albumart"] = None 50 | self.input_cover_art = { 51 | "mime": "image/jpeg", 52 | "type": 3, 53 | "ext": None, 54 | "data": None, 55 | } 56 | 57 | if not self.__tag_mapping: 58 | self.__tag_mapping.update(TagMap().as_dict()) 59 | 60 | self.extract_tags() 61 | 62 | def extract_tags(self): 63 | """ 64 | extracts metadata tags from the audio file 65 | """ 66 | ext = os.path.splitext(self.input_file)[1].lower() 67 | if ext in self.__tag_mapping: 68 | tags = mutagen.File(self.input_file) 69 | for tag, key in self.__tag_mapping[ext].items(): 70 | if tag == "albumart": 71 | self._extract_album_art(ext, tags) 72 | elif key in tags: 73 | self.input_tags[tag] = tags[key][0] 74 | elif tag == "lyrics" and key == "USLT": 75 | self.input_tags.update( 76 | { 77 | tag: tags[id3tag].text 78 | for id3tag in tags 79 | if id3tag.startswith(key) 80 | } 81 | ) 82 | 83 | def _extract_album_art(self, ext, tags): 84 | album_art_tag = self.__tag_mapping.get(ext, {}).get("albumart") 85 | if album_art_tag is not None: 86 | if album_art_tag in tags: 87 | self.input_cover_art["ext"] = ext 88 | if ext == ".mp3": 89 | image = tags[album_art_tag] 90 | self.input_cover_art["mime"] = image.mime 91 | self.input_cover_art["data"] = image.data 92 | elif ext == ".m4a": 93 | self.input_cover_art["data"] = tags[album_art_tag][0] 94 | elif ext in (".ogg", ".flac"): 95 | encoded_image = tags[album_art_tag][0] 96 | image = mutagen.flac.Picture(base64.b64decode(encoded_image)) 97 | self.input_cover_art["data"] = image.data 98 | self.input_cover_art["mime"] = image.mime 99 | elif ext == ".mp3": 100 | for key in tags: 101 | if key.startswith(album_art_tag): 102 | image = tags[key] 103 | self.input_cover_art["mime"] = image.mime 104 | self.input_cover_art["data"] = image.data 105 | 106 | def insert_tags(self, output_file): 107 | """ 108 | inserts tags tags into an audio file. 109 | 110 | :param output_file: output file name. 111 | """ 112 | ext = os.path.splitext(output_file)[1].lower() 113 | try: 114 | tag_map = self.__tag_mapping[ext] 115 | except KeyError: 116 | return 117 | 118 | try: 119 | output_tags = self.__opener[ext](output_file) 120 | except mutagen.apev2.APENoHeaderError: 121 | output_tags = self.__opener[ext]() 122 | except KeyError: 123 | return 124 | 125 | for input_tag_name, input_tag_value in self.input_tags.items(): 126 | if input_tag_value is None or input_tag_name not in tag_map: 127 | continue 128 | output_tag_name = tag_map[input_tag_name] 129 | 130 | if input_tag_name in ("tracknumber", "disk"): 131 | if ( 132 | isinstance(input_tag_value, (list, tuple)) 133 | and len(input_tag_value) == 2 134 | ): 135 | input_tag_value = "%d/%d" % tuple(input_tag_value) 136 | 137 | if ext == ".mp3": 138 | if input_tag_name == "lyrics": 139 | output_tags[output_tag_name] = self.__id3_mapping[input_tag_name]( 140 | encoding=3, 141 | lang="eng", 142 | desc="lyrics", 143 | text="%s" % input_tag_value, 144 | ) 145 | else: 146 | output_tags[output_tag_name] = self.__id3_mapping[input_tag_name]( 147 | encoding=3, text=["%s" % input_tag_value] 148 | ) 149 | elif ext in self.__tag_mapping and ext != ".mp3": 150 | if input_tag_name == "tracknumber" and ext == ".m4a": 151 | if "/" in str(input_tag_value): 152 | track_number = [int(i) for i in str(input_tag_value).split("/")] 153 | output_tags[output_tag_name] = [ 154 | (track_number[0], track_number[1]) 155 | ] 156 | else: 157 | output_tags[output_tag_name] = [(int(input_tag_value), 0)] 158 | else: 159 | output_tags[output_tag_name] = ["%s" % input_tag_value] 160 | 161 | if ext == ".wv": 162 | output_tags.save(output_file) 163 | else: 164 | output_tags.save() 165 | 166 | self._insert_album_art(ext, output_file) 167 | 168 | def _insert_album_art(self, ext, output_file): 169 | if self.input_cover_art["data"] is None: 170 | return 171 | 172 | if ext == ".m4a": 173 | output_tags = mutagen.mp4.MP4(output_file) 174 | if self.input_cover_art["ext"] == ".mp3": 175 | if self.input_cover_art["mime"] == "image/png": 176 | mime = mutagen.mp4.MP4Cover.FORMAT_PNG 177 | else: 178 | mime = mutagen.mp4.MP4Cover.FORMAT_JPEG 179 | image = mutagen.mp4.MP4Cover(self.input_cover_art["data"], mime) 180 | output_tags["covr"] = [image] 181 | output_tags.save() 182 | elif ext == ".mp3": 183 | audio = mutagen.mp3.MP3(output_file, ID3=mutagen.id3.ID3) 184 | if self.input_cover_art["ext"] in (".m4a", ".ogg", ".flac"): 185 | image = mutagen.id3.APIC( 186 | desc="", 187 | encoding=3, 188 | data=self.input_cover_art["data"], 189 | type=self.input_cover_art["type"], 190 | mime=self.input_cover_art["mime"], 191 | ) 192 | audio.tags.add(image) 193 | audio.save() 194 | elif ext == ".flac": 195 | tags = mutagen.File(output_file) 196 | image = mutagen.flac.Picture() 197 | image.desc = "" 198 | image.data = self.input_cover_art["data"] 199 | image.type = self.input_cover_art["type"] 200 | image.mime = self.input_cover_art["mime"] 201 | tags.add_picture(image) 202 | tags.save() 203 | -------------------------------------------------------------------------------- /ftransc/utils.py: -------------------------------------------------------------------------------- 1 | import os 2 | import re 3 | import pathlib 4 | import optparse 5 | import multiprocessing 6 | 7 | import urllib.parse 8 | 9 | import ftransc 10 | from ftransc.config import AudioPresets 11 | from ftransc.errors import AudioPresetError 12 | 13 | 14 | def get_audio_presets(audio_format, audio_quality="normal", external_encoder=False): 15 | """ 16 | Gets audio presets for a given audio format and audio quality from the config file. 17 | 18 | :param audio_format: 19 | :param audio_quality: 20 | :param external_encoder: 21 | :return: 22 | """ 23 | 24 | default_audio_quality = "normal" 25 | audio_presets = AudioPresets().as_dict() 26 | encoder_type = "ext" if external_encoder else "int" 27 | audio_preset_name = "{0}_{1}".format(audio_format, encoder_type) 28 | if audio_preset_name not in audio_presets: 29 | fallback_encoder_type = "ext" if encoder_type == "int" else "int" 30 | fallback_audio_preset_name = "{0}_{1}".format( 31 | audio_format, fallback_encoder_type 32 | ) 33 | if fallback_audio_preset_name not in audio_presets: 34 | raise AudioPresetError( 35 | "The audio format [%s] has no audio preset." % audio_format 36 | ) 37 | audio_preset_name = fallback_audio_preset_name 38 | 39 | selected_audio_preset = audio_presets[audio_preset_name] 40 | if audio_quality not in selected_audio_preset: 41 | audio_quality = default_audio_quality 42 | return selected_audio_preset[audio_quality] 43 | 44 | 45 | def get_audio_formats(): 46 | audio_presets = AudioPresets().as_dict() 47 | return sorted(list({key.split("_")[0].strip() for key in audio_presets})) 48 | 49 | 50 | def parse_args(version=ftransc.__version__): 51 | parser = optparse.OptionParser(usage="%prog [options] [files]", version=version) 52 | parser.add_option( 53 | "-f", "--format", type=str, default="mp3", help="audio format to convert to" 54 | ) 55 | parser.add_option( 56 | "-q", "--quality", type=str, default="normal", help="audio quality preset" 57 | ) 58 | parser.add_option( 59 | "-c", "--check", dest="check", action="store_true", help="check dependencies" 60 | ) 61 | parser.add_option( 62 | "-r", 63 | "--remove", 64 | dest="remove", 65 | action="store_true", 66 | help="remove original file after converting successfully", 67 | ) 68 | parser.add_option( 69 | "-d", 70 | "--decode", 71 | dest="decode", 72 | action="store_true", 73 | help="decode file .wav format", 74 | ) 75 | parser.add_option( 76 | "-w", 77 | "--over", 78 | dest="overwrite", 79 | action="store_true", 80 | help="overwrite destination file if it exists already", 81 | ) 82 | parser.add_option( 83 | "-u", 84 | "--unlock", 85 | dest="unlock", 86 | action="store_true", 87 | help="unlock a locked file and convert", 88 | ) 89 | parser.add_option( 90 | "--directory", 91 | dest="walk", 92 | type=str, 93 | help="convert all files inside the given directory", 94 | ) 95 | parser.add_option("-o", "--outdir", help="Put converted file into specified folder") 96 | parser.add_option( 97 | "--cd", 98 | "--cdrip", 99 | dest="cdrip", 100 | action="store_true", 101 | default=False, 102 | help="rip Compact Disc (CD) digital audio", 103 | ) 104 | parser.add_option( 105 | "--list-formats", 106 | dest="list_formats", 107 | action="store_true", 108 | default=False, 109 | help="Show available audio formats to convert to", 110 | ) 111 | parser.add_option( 112 | "-p", 113 | "--processes", 114 | dest="num_procs", 115 | default=0, 116 | type=int, 117 | help="Use the specified number of parallel processes. CPU count is the maximum.", 118 | ) 119 | parser.add_option( 120 | "-x", 121 | "--ext-encoder", 122 | action="store_true", 123 | dest="external_encoder", 124 | help="Use external encoder (if available)", 125 | ) 126 | parser.add_option("--debug", action="store_true", help="Show debug messages.") 127 | parser.add_option( 128 | "-s", "--silent", action="store_true", help="Be very less verbose." 129 | ) 130 | parser.add_option( 131 | "--force-root", 132 | action="store_true", 133 | dest="force_root", 134 | help="Take the risk and enable running ftransc as root user" 135 | ) 136 | return parser.parse_args() 137 | 138 | 139 | def rip_compact_disc(): 140 | base_dir = pathlib.Path("~/ftransc/ripped_albums").expanduser().absolute() 141 | base_dir.mkdir(parents=True, exist_ok=True) 142 | _, child_folders, _ = next(os.walk(base_dir)) 143 | 144 | child_dir = base_dir.joinpath(f"CD-{len(child_folders) + 1}") 145 | child_dir.mkdir(parents=True, exist_ok=True) 146 | 147 | os.chdir(child_dir) 148 | print("Ripping Compact Disc (CD)...") 149 | os.system("cdparanoia -B >/dev/null 2>&1") 150 | print("Finished ripping CD") 151 | 152 | _, _, child_files = next(os.walk(child_dir)) 153 | return child_files 154 | 155 | 156 | def is_url(url): 157 | return ( 158 | url 159 | and not os.path.isfile(url) 160 | and isinstance(url, str) 161 | and (url.startswith("http://") or url.startswith("https://")) 162 | ) 163 | 164 | 165 | def is_youtube_playlist(url): 166 | return is_url(url) and urllib.parse.urlparse(url).path.startswith("/playlist") 167 | 168 | 169 | def get_safe_filename(filename): 170 | if not filename: 171 | return filename 172 | regex = re.compile(r'[\s)(\]\[}{> 0: 185 | if contains_youtube_playlist: 186 | return desired_number_of_workers 187 | return min([desired_number_of_workers, number_of_files]) 188 | if number_of_files < num_processes: 189 | if has_youtube_playlist(files): 190 | return num_processes 191 | return number_of_files 192 | return num_processes 193 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | description-file = README.md 3 | 4 | [bdist_wheel] 5 | universal = 1 6 | 7 | [flake8] 8 | max-line-length = 100 9 | inline-quotes = double 10 | 11 | [isort] 12 | balanced_wrapping = true 13 | line_length = 100 14 | lines_after_imports = 2 15 | multi_line_output = 3 16 | not_skip=__init__.py 17 | order_by_type = true 18 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | version = "7.0.3" 4 | 5 | requirements = [ 6 | "pafy", 7 | "mutagen", 8 | "plumbum", 9 | "blessings", 10 | "youtube-dl", 11 | ] 12 | 13 | with open("README.md") as fd: 14 | long_description = fd.read() 15 | 16 | if "a" in version: 17 | dev_status = "3 - Alpha" 18 | elif "b" in version: 19 | dev_status = "4 - Beta" 20 | else: 21 | dev_status = "5 - Production/Stable" 22 | 23 | setup_args = { 24 | "name": "ftransc", 25 | "version": version, 26 | "author": "Mkhanyisi Madlavana", 27 | "author_email": "mkhanyisi@gmail.com", 28 | "url": "https://github.com/dopstar/ftransc", 29 | "download_url": "https://github.com/dopstar/ftransc/tarball/{0}".format(version), 30 | "package_dir": {"ftransc": "ftransc"}, 31 | "description": "ftransc is a python library for converting audio files across various formats.", 32 | "long_description": long_description, 33 | "long_description_content_type": "text/markdown", 34 | "packages": [ 35 | "ftransc", 36 | "ftransc.core", 37 | "ftransc.config", 38 | "ftransc.metadata", 39 | "ftransc.core.queue", 40 | ], 41 | "package_data": {"ftransc": ["*.md", "config/data/*.json"]}, 42 | "install_requires": requirements, 43 | "keywords": "Audio, Convert, ffmpeg, avconv, mp3", 44 | "classifiers": [ 45 | "Development Status :: {0}".format(dev_status), 46 | "Intended Audience :: End Users/Desktop", 47 | "Natural Language :: English", 48 | "Programming Language :: Python :: 3.6", 49 | "Programming Language :: Python :: 3.7", 50 | "Programming Language :: Python :: 3.8", 51 | "Programming Language :: Python :: 3.9", 52 | ], 53 | "entry_points": {"console_scripts": ["ftransc=ftransc.launcher:cli"]}, 54 | } 55 | 56 | setup(**setup_args) 57 | -------------------------------------------------------------------------------- /static/images/ftransc_cli.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dopstar/ftransc/4398097f9c7c412b6f6d7af4307b04dda69326dd/static/images/ftransc_cli.png -------------------------------------------------------------------------------- /static/images/ftransc_gui.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dopstar/ftransc/4398097f9c7c412b6f6d7af4307b04dda69326dd/static/images/ftransc_gui.png -------------------------------------------------------------------------------- /static/images/ftransc_gui_v4.1.4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dopstar/ftransc/4398097f9c7c412b6f6d7af4307b04dda69326dd/static/images/ftransc_gui_v4.1.4.png -------------------------------------------------------------------------------- /static/images/ftransc_nautilus-scripts.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dopstar/ftransc/4398097f9c7c412b6f6d7af4307b04dda69326dd/static/images/ftransc_nautilus-scripts.png -------------------------------------------------------------------------------- /static/images/rb_plugin0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dopstar/ftransc/4398097f9c7c412b6f6d7af4307b04dda69326dd/static/images/rb_plugin0.png -------------------------------------------------------------------------------- /static/images/rb_plugin1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dopstar/ftransc/4398097f9c7c412b6f6d7af4307b04dda69326dd/static/images/rb_plugin1.png -------------------------------------------------------------------------------- /static/images/rb_plugin2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dopstar/ftransc/4398097f9c7c412b6f6d7af4307b04dda69326dd/static/images/rb_plugin2.png -------------------------------------------------------------------------------- /tests/fixtures/presets.json: -------------------------------------------------------------------------------- 1 | { 2 | "ogg_int": { 3 | "insane" : "-vn -ac 2 -acodec libvorbis -q 10", 4 | "extreme": "-vn -ac 2 -acodec libvorbis -q 8", 5 | "high" : "-vn -ac 2 -acodec libvorbis -q 6", 6 | "normal" : "-vn -ac 2 -acodec libvorbis -q 3", 7 | "low" : "-vn -ac 2 -acodec libvorbis -q 1", 8 | "tiny" : "-vn -ac 1 -acodec libvorbis -q 0 -ar 32000" 9 | }, 10 | "mp3_int": { 11 | "insane" : "-vn -ac 2 -acodec libmp3lame -ab 320k", 12 | "extreme": "-vn -ac 2 -acodec libmp3lame -aq 0", 13 | "high" : "-vn -ac 2 -acodec libmp3lame -aq 2", 14 | "normal" : "-vn -ac 2 -acodec libmp3lame -aq 4", 15 | "low" : "-vn -ac 2 -acodec libmp3lame -aq 9", 16 | "tiny" : "-vn -ac 1 -acodec libmp3lame -ab 32k -ar 32000" 17 | }, 18 | "m4a_int": { 19 | "insane" : "-vn -ac 2 -acodec libfaac -aq 250", 20 | "extreme": "-vn -ac 2 -acodec libfaac -aq 220", 21 | "high" : "-vn -ac 2 -acodec libfaac -aq 170", 22 | "normal" : "-vn -ac 2 -acodec libfaac -aq 100", 23 | "low" : "-vn -ac 2 -acodec libfaac -aq 50", 24 | "tiny" : "-vn -ac 1 -acodec libfaac -aq 30 -ar 32000" 25 | }, 26 | "wma_int": { 27 | "insane" : "-vn -ac 2 -acodec wmav2 -ab 320k", 28 | "extreme": "-vn -ac 2 -acodec wmav2 -ab 256k", 29 | "high" : "-vn -ac 2 -acodec wmav2 -ab 192k", 30 | "normal" : "-vn -ac 2 -acodec wmav2 -ab 128k", 31 | "low" : "-vn -ac 2 -acodec wmav2 -ab 64k", 32 | "tiny" : "-vn -ac 1 -acodec wmav2 -ab 32k -ar 32000" 33 | }, 34 | "flac_int": { 35 | "insane" : "-vn -acodec flac -compression_level 10", 36 | "extreme": "-vn -acodec flac -compression_level 8", 37 | "high" : "-vn -acodec flac -compression_level 7", 38 | "normal" : "-vn -acodec flac -compression_level 5", 39 | "low" : "-vn -acodec flac -compression_level 3", 40 | "tiny" : "-vn -acodec flac -compression_level 1" 41 | }, 42 | "avi_int": { 43 | "insane" : "-vcodec mpeg4 -vtag divx -vb 2000k -acodec libmp3lame -ac 2 -ab 128k", 44 | "extreme": "-vcodec mpeg4 -vtag divx -vb 1700k -acodec libmp3lame -ac 2 -ab 128k", 45 | "high" : "-vcodec mpeg4 -vtag divx -vb 1400k -acodec libmp3lame -ac 2 -ab 128k", 46 | "normal" : "-vcodec mpeg4 -vtag divx -vb 1300k -acodec libmp3lame -ac 2 -ab 128k", 47 | "low" : "-vcodec mpeg4 -vtag divx -vb 900k -acodec libmp3lame -ac 2 -ab 128k", 48 | "tiny" : "-vcodec mpeg4 -vtag divx -vb 600k -acodec libmp3lame -ac 2 -ab 128k" 49 | }, 50 | "mpc_ext": { 51 | "insane" : "--overwrite --braindead", 52 | "extreme": "--overwrite -- insane", 53 | "high" : "--overwrite --extreme", 54 | "normal" : "--overwrite --normal", 55 | "low" : "--overwrite --telephone", 56 | "tiny" : "--overwrite --quality 1.00" 57 | }, 58 | "wv_ext": { 59 | "insane" : "-i -y -hh", 60 | "extreme": "-i -y -hh", 61 | "high" : "-i -y -h", 62 | "normal" : "-i -y", 63 | "low" : "-i -y -f", 64 | "tiny" : "-i -y -f" 65 | }, 66 | "ogg_ext": { 67 | "insane" : "-q 10", 68 | "extreme": "-q 8", 69 | "high" : "-q 6", 70 | "normal" : "-q 3", 71 | "low" : "-q 1", 72 | "tiny" : "-q -1" 73 | }, 74 | "mp3_ext": { 75 | "insane" : "--cbr -b 320", 76 | "extreme": "-V 0", 77 | "high" : "-V 2", 78 | "normal" : "-V 4", 79 | "low" : "--preset 64", 80 | "tiny" : "--cbr -b 32 -m m -s 32" 81 | }, 82 | "flac_ext": { 83 | "insane" : "--best", 84 | "extreme": "-7", 85 | "high" : "-6", 86 | "normal" : "-5", 87 | "low" : "-3", 88 | "tiny" : "--fast" 89 | }, 90 | "m4a_ext": { 91 | "insane" : "-w -q 500", 92 | "extreme": "-w -q 350", 93 | "high" : "-w -q 200", 94 | "normal" : "-w", 95 | "low" : "-w -q 50", 96 | "tiny" : "-w -q 10" 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /tests/test_utils.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | import os 5 | import json 6 | import unittest 7 | 8 | import ftransc.utils as futils 9 | 10 | 11 | class UtilsTestCase(unittest.TestCase): 12 | @classmethod 13 | def setUpClass(cls): 14 | fixture_filename = os.path.abspath( 15 | os.path.join(os.path.dirname(__file__), "fixtures", "presets.json") 16 | ) 17 | with open(fixture_filename) as fd: 18 | cls.presets = json.load(fd) 19 | 20 | def test_presets(self): 21 | for audio_format, preset in self.presets.items(): 22 | is_external = audio_format.endswith("_ext") 23 | audio_format = audio_format.split("_")[0] 24 | for audio_quality, quality_preset in preset.items(): 25 | self.assertEqual( 26 | futils.get_audio_presets(audio_format, audio_quality, is_external), 27 | quality_preset, 28 | ) 29 | 30 | 31 | if __name__ == "__main__": 32 | unittest.main() 33 | --------------------------------------------------------------------------------