Store video comments, fix set file button in video edit window

This commit is contained in:
A S Lewis 2021-08-05 15:39:53 +01:00
parent 4185b190f1
commit 8e098f076f
39 changed files with 3399 additions and 1883 deletions

128
CHANGES
View File

@ -23,8 +23,8 @@ MAJOR NEW FEATURES
automatically
- A 'Custom download all' button can be added to the Videos tab, beneath the
'Download all' button, if it's convenient for you. Click 'Edit > System
preferences... > Windows > Main window' and select 'Show a Custom Downlaod
all button in the Videos Tab'
preferences... > Windows > Main window' and select 'Show a Custom Download
all button in the Videos tab'
- Until now, Tartube has only been able to download videos into its own
data folder. You can now download channels and playlists to any location on
your filesystem. This is not recommended unless you have a good reason (see
@ -131,7 +131,7 @@ MINOR NEW FEATURES
Click 'Edit > General download options...'. Click the 'Show advanced
download options' button if it's visible. Then click 'Advanced >
Configurations'
- All download options in the edit window ('Edit > General downlaod
- All download options in the edit window ('Edit > General download
options...') now have a tooltip, showing the actual youtube-dl/yt-dlp
option
- Added a range of download options for yt-dlp only (which cannot be used with
@ -301,7 +301,7 @@ MINOR NEW FEATURES
button, any duplicate URLs (which are not copied from the top half to the
bottom half) can optionally be deleted now. Click Edit > System
preferences... > Windows > Main window, and select 'In the Classic Mode
Tab, when adding URLs, remove duplicates rather than retaining them' (Git
tab, when adding URLs, remove duplicates rather than retaining them' (Git
#233)
- The first error generated when downloading a video/channel/playlist is now
visible in the tooltip (in both the Progress and Classic Mode tabs). The
@ -309,19 +309,19 @@ MINOR NEW FEATURES
> Main Window, and deselect 'Show errors/warnings in tooltips'. This is a
compromise for showing the full error message in the tabs, which is
not practical due to youtube-dl(c) limitations (Git #233)
- In the Classic Mode Tab, after a download has finished, the name of the
- In the Classic Mode tab, after a download has finished, the name of the
video file is no longer cleared, in order to assist with identifying
failed downloads (Git #233)
- In the Videos Tab, all status icons are now visible for all videos, even when
- In the Videos tab, all status icons are now visible for all videos, even when
thumbnails are not drawn (Git #233)
- In the Classic Mode Tab, added a new 'Clear downloaded' button (Git #233)
- In the Classic Mode tab, added a new 'Clear downloaded' button (Git #233)
MAJOR FIXES
- Fixed the 'No translation file found for domain: base' crashes (Git #245,
#247)
- Apparent fix for crashes while downloading videos from LinkedIn Video. The
fix has not been fully tested yet (Git #240)
- Fixed the re-download button in the Classic Mode Tab, which was completely
- Fixed the re-download button in the Classic Mode tab, which was completely
broken
- Fixed several problems with translations, which only became apparent after
someone submitted a translation file
@ -404,7 +404,7 @@ MAJOR FIXES
MINOR FIXES
- After a period of continuous operation, without checking/download videos,
live/debut videos could change their status, but the order of videos in the
Videos Tab was not updated. Live/debut videos are supposed to be listed
Videos tab was not updated. Live/debut videos are supposed to be listed
first, before all other videos; this should now be working better
- Times in the Errors/Warnings tab were shown in UTC, instead of the user's
local time zone. The same problem applied to metadata in Tartube's saved
@ -571,7 +571,7 @@ MINOR NEW FEATURES
(so they don't appear in the Errors/Warnings tab). They can be seen in
Edit > System preferences... > Windows > Websites (Git #172)
- Git #169 reported that Tartube was showing a negative number of videos in the
Video Index (left-hand side of the Videos Tab). The cause was not found,
Video Index (left-hand side of the Videos tab). The cause was not found,
but Tartube now automatically detects this kind of errors and auto-fixes
it. The database integrity check has also been updated
- The youtube-dl download options '--sleep-interval' and
@ -579,7 +579,7 @@ MINOR NEW FEATURES
(Git #173)
- The layout of edit and preferences has been updated and improved, in many
cases
- In the Videos Tab toolbar, there was no button to cancel filtering by text;
- In the Videos tab toolbar, there was no button to cancel filtering by text;
added one. Some issues with these buttons being enabled/disabled at the
wrong time were also fixed
- If the download of a video/channel/playlist stalls, for some reason, there is
@ -598,20 +598,20 @@ MINOR NEW FEATURES
download options'
- You can no longer open multiple preference windows, or multiple edit windows
for the same set of data
- In the Classic Mode Tab, Tartube can now remember URLs that have been added,
- In the Classic Mode tab, Tartube can now remember URLs that have been added,
but not yet used. To enable this feature, click the menu button in the
top-right corner, and select 'Remember URLs'. When you restart Tartube,
any URLs which were not downloaded in the previous session should now be
visible in the top half of the tab (Git #194)
- In the Classic Mode Tab, there is new 'Clear all' button (Git #194)
- Added a slider in the middle of the Classic Mode Tab, so that the two halves
can be resized, if required. The existing sliders in the Videos Tab and in
the Progress Tab now have a minimum size, so that the user can't
- In the Classic Mode tab, there is new 'Clear all' button (Git #194)
- Added a slider in the middle of the Classic Mode tab, so that the two halves
can be resized, if required. The existing sliders in the Videos tab and in
the Progress tab now have a minimum size, so that the user can't
accidentally make half of the window invisible
- The Gtk file chooser dialogue was typically bigger than the size of the
observable universe (especially on MS Windows). Tartube will now resize it,
if so
- Custom downloads can now be performed in the Classic Mode Tab. To enable
- Custom downloads can now be performed in the Classic Mode tab. To enable
them, click the menu icon in the top-right corner, and select 'Enable
custom downloads'. Then, when you click the 'Download all' button in the
bottom-right corner, a custom download is performed. For more information
@ -620,7 +620,7 @@ MINOR NEW FEATURES
MAJOR FIXES
- Fixed various problems caused on MS Windows when downloading videos whose
names contain Japanese characters (Git #106, #115, #175)
- Some reports suggest that Tartube crashes when the Output Tab contains a
- Some reports suggest that Tartube crashes when the Output tab contains a
great deal of text (tens of thousands of lines). The problem could not be
reproduced, but there is now a maximum page size. The maximum size can be
adjusted by the user. No further problems have been reported (Git #170)
@ -628,7 +628,7 @@ MAJOR FIXES
be displayed in a much more consistent order, which in most cases fixes the
issues
- In case of further problems in sorting videos into their correct order, the
toolbar at the bottom of the Videos Tab has a new button which will force
toolbar at the bottom of the Videos tab has a new button which will force
a re-sort of the visible video list
- Fixed an error when opening a directory/folder (containing downloaded videos,
etc) on the desktop (Git #180)
@ -650,7 +650,7 @@ MAJOR FIXES
- Fixed a crash caused by a faulty setting of a video's livestream status
(Git #34)
- Fixed a crash caused when videos/channels/playlists are automatically removed
from the bottom half of the Progress Tab (Git #34)
from the bottom half of the Progress tab (Git #34)
- Fixed incorrect handling of a video's URL, when the video is dragged from an
external application (such as a file explorer) into Tartube's main window
(on Linux/BSD only). Tartube now recognises both a file path and a URL,
@ -670,7 +670,7 @@ MINOR FIXES
example, an unplugged external hard drive) (Git #167)
- In the preferences window, selecting multiple databases at the same time
caused Gtk issues, so disabled multiple selection, which fixes the issues
- In the Results List (bottom half of the Progress Tab), a deleted video could
- In the Results List (bottom half of the Progress tab), a deleted video could
still be selected, and the user might still try to right-click it and
delete it again. The code has been updated so that any video visible in the
list that has been deleted cannot be selected or right-clicked
@ -682,7 +682,7 @@ MINOR FIXES
- In the Video Catalogue, fixed the missing gap between the 'Favourite' and
'Missing' labels. Fixed the situation in which that line became too long
for its box
- In the Videos Tab toolbar, when the user sets a recent date, the Video
- In the Videos tab toolbar, when the user sets a recent date, the Video
Catalogue no longer tries to skip to the non-existence page zero
- The main window's menu now refers to the actual downloader (for example,
youtube-dlc), rather than referring to youtube-dl until Tartube restarts
@ -691,15 +691,15 @@ MINOR FIXES
- Tartube could not recognise some youtube-dlc version numbers. Fixed
- Made minor changes to some icons to improve legibility
- Fixed a Python error when right-clicking unselected videos in the Classic
Mode Tab
Mode tab
- For livestreams that are already broadcasting, the 'D/L on start' label is no
longer clickable
- After clicking File > Save all, the user will now see a better confirmation
dialogue
- Fixed a minor spacing issue in the tooltip text used for videos
- In the toolbar at the bottom of the Videos Tab, the next/previous buttons
- In the toolbar at the bottom of the Videos tab, the next/previous buttons
were the wrong way around (but only when custom icons were in use). Fixed
- Fixed some issues in the Output Tab, in which the scrollbar did not
- Fixed some issues in the Output tab, in which the scrollbar did not
automatically scroll to the bottom as new text was added. (The behaviour is
still not perfect on all operating systems, but it is better than before)
- The cookie jar used by youtube-dl is now written to Tartube's data directory,
@ -716,7 +716,7 @@ MAJOR NEW FEATURES
by youtube-dl, or by Tartube itself, as appropriate). This only affects new
downloads. If you want to convert .webp thumbnails you've already
downloaded, click Operations > Tidy up files..., select 'Convert .webp
thumbnails to .jpg using Ffmpeg', and click OK. (This procedure may take a
thumbnails to .jpg using FFmpeg', and click OK. (This procedure may take a
while if there are thousands of thumbnails to convert) (Git #155 and
others)
- Thumbnails, video description, metadata and annotation files can now be
@ -746,9 +746,9 @@ MAJOR NEW FEATURES
if it is already installed on your system. This will benefit users of the
.DEB and .RPM packages, who until now were expected to know how to set the
youtube-dl path manually (Git #152)
- Ffmpeg and AVConv will now also be auto-detected. If you have installed them
- FFmpeg and AVConv will now also be auto-detected. If you have installed them
in unusual locations, you should specify those locations in Edit >
System preferences... > youtube-dl > Ffmpeg / AVConv. If not, there is no
System preferences... > youtube-dl > FFmpeg / AVConv. If not, there is no
need to specify either location; just leave the boxes empty. (None of this
applies to MS Windows users)
- When Tartube shuts down unexpectedly, it doesn't have time to mark the
@ -772,8 +772,8 @@ MINOR NEW FEATURES
automatically makes the Output tab visible. Hopefully this will avoid
confusion for new users, who do not notice that the Check all/Download all
buttons have been greyed out. This behaviour can be disabled, if required:
click Edit > System preferences... > Output > Output Tab, and deselect
'During an update operation, automatically switch to the Output Tab'
click Edit > System preferences... > Output > Output tab, and deselect
'During an update operation, automatically switch to the Output tab'
(Git #149)
- The invidio.us website has closed. There are many mirrors available. Tartube
now uses invidious.site as its default mirror. To specify a different
@ -860,7 +860,7 @@ ATTENTION YOUTUBE USERS
format, .webp. The Gtk graphics libraries don't support this format. A
future release of youtube-dl will convert .webp thumbnails to .jpg, but the
feature is not available yet. When it becomes available, you might need to
install Ffmpeg on your system, if you haven't already done so (see the
install FFmpeg on your system, if you haven't already done so (see the
README file)
MAJOR NEW FEATURES
@ -896,7 +896,7 @@ MINOR NEW FEATURES
- In the Classic Mode tab, the buttons in the top-right corner have been
replaced with a popup menu
- A separate set of download options are now applied to downloads in the
Classic Mode Tab. These download options are applied by default, even on
Classic Mode tab. These download options are applied by default, even on
existing installations. To disable/re-enable them, click the menu in the
top-right corner of the tab, and select 'Use classic download options' or
'Use general download options'
@ -1010,7 +1010,7 @@ v2.1.0 (7 May 2020)
MAJOR NEW FEATURES
- For everyone who wants a simpler way to download videos, a new Classic Mode
Tab has been added, emulating the look and feel of youtube-dl-gui. Videos
tab has been added, emulating the look and feel of youtube-dl-gui. Videos
downloaded in this tab can be downloaded to any location, and are not added
to Tartube's database
- Tartube can now detect livestreams, and alert you when they start. This
@ -1029,7 +1029,7 @@ MINOR NEW FEATURES
- Slightly improved the functionality of buttons in the system preference
window's database tab
- If a database can't be loaded (but an alternative database can), an
explanatory messages is now added to the Errors/Warnings Tab. If an
explanatory messages is now added to the Errors/Warnings tab. If an
alternative database can't be loaded (or only one database has been added
to Tartube's list), then the dialogue window seen by the user is now
slightly more helpful
@ -1045,7 +1045,7 @@ MAJOR FIXES
- The MS Windows installers have been updated to use Python 3.8. This may fix
some stability issues for a few users
- For systems with a broken Gtk library (or if the user has disabled minor
cosmetic features anyway), the list of videos in the Videos Tab is no
cosmetic features anyway), the list of videos in the Videos tab is no
longer updated during a download operation. This should resolve some
lingering stability issues. (You can manually update the list by selecting
a different channel/playlist/folder, then selecting the original one again)
@ -1117,7 +1117,7 @@ MAJOR NEW FEATURES
The 'STRICT' packages are compiled using new environment variables,
TARTUBE_PKG and TARTUBE_PKG_STRICT (replacing the old TARTUBE_DEBIAN)
environment variable. See the comments in setup.py for more details
- During a download operation, in the Progress Tab, you can now right-click a
- During a download operation, in the Progress tab, you can now right-click a
video and select 'Stop after these videos'. This allows all of the current
video downloads to finish, before halting the download operation
- The download options window (in the Formats tab) did not allow users to
@ -1191,7 +1191,7 @@ MAJOR NEW FEATURES
can now perform a test download. First, click 'Operations > Test
youtube-dl...' (or right-click a video in the main window's list). Copy the
video's URL into the dialogue window, and then click the OK button. Click
the Output Tab to see the results. If the test successfully downloads the
the Output tab to see the results. If the test successfully downloads the
video, then the problem was with Tartube. If the test fails to download the
video, then the problem is with the underlying youtube-dl software (or with
the video website)
@ -1219,15 +1219,15 @@ MAJOR NEW FEATURES
from the 'Waiting Videos' folder (this doesn't happen to bookmarked videos)
- The previous version was unable to delete a channel, playlist or folder (see
below). That error caused a partially-deleted channel/playlist/folder to
appear in the Videos Tab, on the left-hand side. In case similar errors
appear in the Videos tab, on the left-hand side. In case similar errors
occur in the future, a feature has been added to look for errors and
inconsistencies in the Tartube database and automatically fix them. Click
'Edit > System preferences... > Filesystem > DB Errors > Check' to use it
- Tartube can now fetch a list of available video formats for a video. Right-
click the video and select 'Fetch > Available formats'. Click the Output
Tab to see the results
tab to see the results
- Tartube can also fetch a list of available subtitles for a video. Right-click
the video and select 'Fecth > Available subtitles'. Click the Output Tab to
the video and select 'Fecth > Available subtitles'. Click the Output tab to
see the results
- Tartube can now remember the size of its main window, and use the same size
when it restarts. This feature is disabled by default. To enable it, click
@ -1365,7 +1365,7 @@ MAJOR NEW FEATURES
- The MS Windows installer now includes a copy of AtomicParsley, so there is no
need to install it yourself. This does not affect Linux/BSD users, who can
continue installing AtomicParsley by the usual methods
- The list in the top half of the Progress Tab is often full, and it's
- The list in the top half of the Progress tab is often full, and it's
sometimes difficult to see what is being downloaded right at this moment.
You can now hide finished rows, if you want to, so that active rows appear
at the top of the list
@ -1404,7 +1404,7 @@ MAJOR FIXES
MINOR NEW FEATURES
- Tartube icons have been updated, in some cases making them easier to identify
- In the Progress Tab, added tooltips to assist with identifying undownloaded
- In the Progress tab, added tooltips to assist with identifying undownloaded
videos (Git #51)
- More types of YouTube error message can now be filtered out
- We have also added a customisable list of strings (or regular expressions);
@ -1441,7 +1441,7 @@ MINOR NEW FEATURES
- Minor improvements to aesthetics for some textviews and treeviews
MINOR FIXES
- Fixed incorrect operation of the checkbuttons in the Errors/Warnings Tab.
- Fixed incorrect operation of the checkbuttons in the Errors/Warnings tab.
Added new checkbuttons to separate Tartube errors/warnings from youtube-dl
errors/warnings (Git #50)
- 'Child process exited with non-zero code' errors still appeared in the
@ -1458,7 +1458,7 @@ MINOR FIXES
custom format was garbled. Fixed, and it should now be working as intended
- During a simulated download, videos which are not in a channel or playlist
(for example, videos in the 'Unsorted Videos' folder) did not appear in
the Results List in the Progress Tab. Fixed
the Results List in the Progress tab. Fixed
- Fixed an unprintable character in the licence declaration, visible in
Tartube's 'About' window
- When deleting a video, Tartube will now delete more related files (such as
@ -1495,14 +1495,14 @@ MAJOR NEW FEATURES
or folder without having to download anything. Right-click a video/channel/
playlist/folder and select 'Downloads > Show system command'
- During a download operation, the system commands used are now visible (by
default) in the Output Tab. The system command can also be displayed in the
default) in the Output tab. The system command can also be displayed in the
terminal, if required; this is disabled by default
MINOR NEW FEATURES
- In the Output Tab, the summary page is now hidden by default. To make it
- In the Output tab, the summary page is now hidden by default. To make it
visible, click 'Edit > System Preferences... > Output >
Show a summary of active threads' and then restart Tartube
- In the Errors/Warnings Tab, added checkbuttons to filter out errors and/or
- In the Errors/Warnings tab, added checkbuttons to filter out errors and/or
warning messages, if required (Git #50)
- In the Progress tab, in the top half of the window, you can now right-click
an unnamed video to open it in your web browser. This will be useful in
@ -1530,7 +1530,7 @@ MINOR FIXES
- The popup menu in the Progress tab, in the top half of the tab, did not work
as intended during a download operation, and again after a download
operation. Fixed both sets of issues
- Coloured text was not displayed in the Output Tab correctly. Fixed
- Coloured text was not displayed in the Output tab correctly. Fixed
v1.3.048 (23 Jan 2020)
-------------------------------------------------------------------------------
@ -1637,7 +1637,7 @@ MAJOR NEW FEATURES
main menu (Operations > Install FFmpeg). Tartube still cannot recognise
the ordinary version of FFmpeg, and it still does not recognise AVConv at
all. It is unlikely that this situation can be remedied
- A new Output Tab has been added, in which you can see what is happening
- A new Output tab has been added, in which you can see what is happening
internally when you check or download videos, update youtube-dl, install
FFmpeg, or refresh the Tartube database. The amount of information shown
can be customised in the System preferences window. The information can
@ -1649,7 +1649,7 @@ MAJOR NEW FEATURES
download (for example, 1080p). You can use the download options window
(Edit > General download options... > Formats, and then choose a video
format like 'any format [1080p]'). You can also use the new spinbutton at
the bottom of the Progress Tab. Both of these methods have the same effect,
the bottom of the Progress tab. Both of these methods have the same effect,
so it's not necessary to use both of them. Tartube will download videos in
that resolution if possible, or in the next highest available resolution
otherwise
@ -1758,8 +1758,8 @@ MAJOR NEW FEATURES
generated by YouTube about the lack of annotations are ignored by default
- For channels/playlists/folders containing many videos, you can now skip to
the first video uploaded after a certain date, using the new button in the
toolbar at the bottom of the Videos Tab
- The lists in the Results Tab can now be right-clicked, so you can change the
toolbar at the bottom of the Videos tab
- The lists in the Results tab can now be right-clicked, so you can change the
order in which videos/channels/playlists/folders are checked/downloaded,
abandon a download, play a video directly from the Results List, delete a
video directly from the Results List, and so on
@ -1775,7 +1775,7 @@ MINOR NEW FEATURES
- You can now select multiple videos in the Video Catalogue, and apply an
action to all of them (by right-clicking them)
- You can now switch to smaller icons in the Video Index (on the left side of
the Videos Tab), if you want to
the Videos tab), if you want to
- You can now force the Video Index to expand its tree whenever you click on a
folder, revealing any channels/playlists it contains. This is disabled by
default
@ -1787,7 +1787,7 @@ MINOR NEW FEATURES
for details of what command to use
- The path to the FFmpeg/AVConv executable can now be specified by the user.
This will be especially helpful for MS Windows users
- Columns in the Progress Tab can now be manually resized
- Columns in the Progress tab can now be manually resized
- The Tartube website can now be opened from the main window menu
- XDG has been added as an optional dependency, for the benefit of Debian
packagers
@ -1806,7 +1806,7 @@ MAJOR FIXES
MINOR FIXES
- Fixed some unicode errors in reading JSON and plain text files
- Fixed the wrong page size displayed in the toolbar at the bototm of the
Videos Tab
Videos tab
- Empty lines in a video's description are now preserved when they're displayed
in Tartube's main window
@ -1824,7 +1824,7 @@ MAJOR NEW FEATURES
contains)
- You can now change the name of a channel, playlist of folder. This doesn't
have any effect on your filesystem; it only changes the name displayed in
the Video Index (the left-hand side of the Videos Tab). This might be
the Video Index (the left-hand side of the Videos tab). This might be
useful for channels and playlists that have weird or very short names. For
example, right-click a folder and select 'Folder actions > Set nickname...'
- You can also rename the channel, playlist or folder, and this action DOES
@ -1833,7 +1833,7 @@ MAJOR NEW FEATURES
click a folder and select 'Filesystem > Rename location...'
- If you change the format of a downloaded video file from the default 'Title'
to, for example, 'Title + ID', the Video catalogue (the right-hand side of
the Videos Tab) will now simply display the video's title (which should be
the Videos tab) will now simply display the video's title (which should be
easier to read). To see the actual filename, you can right-click the video
and select 'Show properties'. This only works if the video's metadata was
downloaded when the video itself was downloaded; this is now turned on by
@ -1853,7 +1853,7 @@ MINOR NEW FEATURES
parent folder (Edit > System preferences... > Windows > When adding
channels/playlists, re-use the optional parent folder)
- When checking/downloading videos, the Results List (the bottom half of the
Progress Tab) can now display videos in reverse order, so you don't have to
Progress tab) can now display videos in reverse order, so you don't have to
scroll down to see the video that was just checked/downloaded (Edit >
System Preferences... > Windows > Show results in reverse order)
- The number of ystem error and warning messages displayed in their own tab
@ -1862,7 +1862,7 @@ MINOR NEW FEATURES
until the 'Clear the list' button is explicitly clicked (Edit >
System preferences... > Windows > Don't remove number of system messages
from tab label until 'Clear' button is clicked)
- Items in the Video Index (on the left-hand side of the Videos Tab) are sorted
- Items in the Video Index (on the left-hand side of the Videos tab) are sorted
alphabetically. The sorting algorithm has been improved to take account of
numbered items, such that '1 Music' will now appear before '11 Comedy'
- For the benefit of package maintainers (such as a Debian package), Tartube
@ -1879,7 +1879,7 @@ MAJOR FIXES
playlist and folder. Your system's Gtk version is now visible in Tartube's
System Preferences window
- After loading the config file, the download limits were set, but not
displayed in the Progress Tab. Fixed
displayed in the Progress tab. Fixed
- Rarely, Tartube crashes (or freezes) when loading a video's JSON metadata
file from your filesystem (but now when downloading it). This should no
longer happen
@ -1942,7 +1942,7 @@ v0.6.0 (4 Jul 2019)
- Occasionally, videos were downloaded (or checked) successfully, but Tartube
failed to notice them. This issue should now be fixed
- When checking videos/channels/playlists/folders, only new videos will now
appear in the Results List (in the 'Progress' Tab)
appear in the Results List (in the Progress tab)
- The toolbar has been redesigned. MS Windows users won't see labels at all
(so everything should fit). Users on all system can turn labels on or off.
Tooltips have been added to the buttons, in case the labels are turned off
@ -1951,7 +1951,7 @@ v0.6.0 (4 Jul 2019)
- Tartube no longer requires the python 'validators' module
- Tartube can now ignore YouTube copyright messages, and also 'Child process
exited with non-zero code' messages, meaning that they won't appear in
Tartube's Errors/Warnings Tab. (They are not ignored by default)
Tartube's Errors/Warnings tab. (They are not ignored by default)
- Tartube now applies a 60-second timeout when youtube-dl tries to download
a video's metadata (since youtube-dl uses a 10-minute timeout); this can be
turned off, if required (#9)
@ -2061,8 +2061,8 @@ v0.2.0 (23 Jun 2019)
- Greatly expanded the README file
- Fixed the constant crashes
- Fixed some Gtk problems (but others remain unfixed)
- Fixed downloads for users who haven't installed Ffmpeg, and for sites that
don't support Ffmpeg
- Fixed downloads for users who haven't installed FFmpeg, and for sites that
don't support FFmpeg
- Several other minor tweaks/fixes
v0.1.0 (27 May 2019)

View File

@ -1,9 +1,9 @@
===================================================
Tartube - The Easy Way To Watch And Download Videos
===================================================
------------------------------------------------------------
Works with YouTube, BitChute, and hundreds of other websites
------------------------------------------------------------
----------------------------------------------------------
Works with YouTube, Odysee, and hundreds of other websites
----------------------------------------------------------
.. image:: screenshots/tartube.png
:alt: Tartube screenshot
@ -31,11 +31,11 @@ Problems can be reported at `our GitHub page <https://github.com/axcore/tartube/
2 Why should I use Tartube?
===========================
- You can fetch a list of videos from your favourite channels and playlists on `YouTube <https://www.youtube.com/>`__, `BitChute <https://www.bitchute.com/>`__, and hundreds of other websites (see `here <https://ytdl-org.github.io/youtube-dl/supportedsites.html>`__ for a full list)
- You can fetch a list of videos from your favourite channels and playlists on `YouTube <https://www.youtube.com/>`__, `Odysee <https://odysee.com/>`__, and hundreds of other websites (see `here <https://ytdl-org.github.io/youtube-dl/supportedsites.html>`__ for a full list)
- If buffering is an issue, you can download a temporary copy of a video before automatically opening it in your favourite media player
- **Tartube** will organise your videos into convenient folders (if that's what you want)
- **Tartube** can alert you when livestreams and debut videos are starting (**YouTube** only)
- If creators upload their videos to more than one website (**YouTube** and **BitChute**, for example), **Tartube** can interact with both sites without creating duplicates
- If creators upload their videos to more than one website (**YouTube** and **Odysee**, for example), **Tartube** can interact with both sites without creating duplicates
- Certain websites operate an "only one opinion allowed" policy. If you think that the censors will remove a video, against the wishes of its creators and before you've had a chance to watch it, **Tartube** can make an archive copy
- Certain websites frequently place restrictions on a video, not because it is unsuitable for some audiences, but for purely political reasons. Tartube can, in some circumstances, see videos that are region-blocked and/or age-restricted
- Certain websites manipulate search results, repeatedly unsubscribe people from their favourite channels and/or deliberately conceal videos that they don't like. **Tartube** won't do any of those things
@ -88,7 +88,7 @@ Source code:
- Start **Tartube**. A setup window should appear
- When prompted, choose a folder in which **Tartube** can store videos
- When prompted, choose a downloader
- On some systems, you will be prompted to install the downloader and/or `Ffmpeg <https://ffmpeg.org/>`__. On other operating systems, you will have to install them yourself
- On some systems, you will be prompted to install the downloader and/or `FFmpeg <https://ffmpeg.org/>`__. On other operating systems, you will have to install them yourself
Tartube can store its videos in a database. If that's what you want, do this:
@ -202,7 +202,7 @@ MacOS users should use the following procedure (with thanks to JeremyShih):
**brew install adwaita-icon-theme**
- It is strongly recommended that you install `Ffmpeg <https://ffmpeg.org/>`__, too
- It is strongly recommended that you install `FFmpeg <https://ffmpeg.org/>`__, too
**brew install ffmpeg**
@ -228,7 +228,7 @@ Linux distributions based on Debian, such as Ubuntu and Linux Mint, can install
2. **Tartube** asks you to choose a data directory, so do that
3. Click **Operations > Update youtube-dl**
It is strongly recommended that you install `Ffmpeg <https://ffmpeg.org/>`__, too. On most Debian-based systems, you can open a terminal window and run this command:
It is strongly recommended that you install `FFmpeg <https://ffmpeg.org/>`__, too. On most Debian-based systems, you can open a terminal window and run this command:
**sudo apt-get install ffmpeg**
@ -254,7 +254,7 @@ On Fedora, the procedure is:
3. Type: ``pip3 install youtube-dl`` or ``pip3 install yt-dlp``
4. You can now run **Tartube**.
It is strongly recommended that you install `Ffmpeg <https://ffmpeg.org/>`__, too. On most RHEL-based systems (for example, Fedora 29-32), you can open a terminal window and run these commands:
It is strongly recommended that you install `FFmpeg <https://ffmpeg.org/>`__, too. On most RHEL-based systems (for example, Fedora 29-32), you can open a terminal window and run these commands:
**sudo dnf -y install https://download1.rpmfusion.org/free/fedora/rpmfusion-free-release-$(rpm -E %fedora).noarch.rpm**
@ -273,7 +273,7 @@ On Arch-based systems. such as Manjaro, Tartube can be installed using the semi-
4. Type: ``makepkg -si``
5. You can now run **Tartube**.
It is strongly recommended that you install `Ffmpeg <https://ffmpeg.org/>`__, too. On most Arch-based systems, you can open a terminal window and run this command:
It is strongly recommended that you install `FFmpeg <https://ffmpeg.org/>`__, too. On most Arch-based systems, you can open a terminal window and run this command:
**sudo pacman -S ffmpeg**
@ -282,7 +282,7 @@ It is strongly recommended that you install `Ffmpeg <https://ffmpeg.org/>`__, to
On Gentoo-based systems, **Tartube** can be installed using the semi-official ebuild package, using the link above.
Tartube requires `youtube-dl <https://youtube-dl.org/>`__. It is strongly recommended that you install `Ffmpeg <https://ffmpeg.org/>`__, too.
Tartube requires `youtube-dl <https://youtube-dl.org/>`__. It is strongly recommended that you install `FFmpeg <https://ffmpeg.org/>`__, too.
If you're not sure how to install using ebuild, then it might be easier to install from PyPI.
@ -330,7 +330,7 @@ These dependencies are optional, but recommended:
- `Python feedparser module <https://pypi.org/project/feedparser/>`__ - enables **Tartube** to detect livestreams
- `Python moviepy module <https://pypi.org/project/moviepy/>`__ - if the website doesn't tell **Tartube** about the length of its videos, moviepy can work it out
- `Python playsound module <https://pypi.org/project/playsound/>`__ - enables **Tartube** to play an alarm when a livestream starts
- `Ffmpeg <https://ffmpeg.org/>`__ - required for various video post-processing tasks; see the section below if you want to use FFmpeg
- `FFmpeg <https://ffmpeg.org/>`__ - required for various video post-processing tasks; see the section below if you want to use FFmpeg
- `AtomicParsley <https://bitbucket.org/wez/atomicparsley/src/default/>`__ - required for embedding thumbnails in audio files
- `aria2 <https://aria2.github.io/>`__ - required for Youtube Stream Capture
- `matplotlib <https://matplotlib.org/>`__ - required for drawing graphs
@ -452,6 +452,7 @@ The procedure used to create the MS Windows installers is described in full in t
* `6.27.4 Removing video slices`_
* `6.27.5 Video slice shortcuts`_
* `6.28 Using youtube-dl forks`_
* `6.29 Video Comments`_
6.1 Setting up Tartube
----------------------
@ -510,7 +511,7 @@ On other systems, users can modify **Tartube**'s settings. There are several loc
**youtube-dl** uses FFmpeg by default, but it can use AVConv for certain tasks.
For more information about **Tartube**'s use of Ffmpeg and AVConv, see `6.25 More information about FFmpeg and AVConv`_.
For more information about **Tartube**'s use of FFmpeg and AVConv, see `6.25 More information about FFmpeg and AVConv`_.
6.4.1 On MS Windows
~~~~~~~~~~~~~~~~~~~
@ -556,6 +557,7 @@ When you start **Tartube** for the first time, there are several folders already
- The **Waiting Videos** folder shows videos that you want to watch soon. When you watch the video, it's automatically removed from the folder (but not from **Tartube**'s database)
- Videos saved to the **Temporary Videos** folder will be deleted when **Tartube** next starts
- The **Unsorted Videos** folder is a useful place to put videos that don't belong to a particular channel or playlist
- The **Video Clips** folder is a useful place to put video clips (see `6.26 Video clips`_)
6.6 Adding videos
-----------------
@ -764,7 +766,7 @@ In fact, you can create as many sets of download options as you like.
.. image:: screenshots/example17.png
:alt: The list of download options
The first item in the list, **general**, is the default set of download options. The second item, **classic**, are the download options that apply in the **Classic Mode** Tab (see `6.22 Classic Mode`_).
The first item in the list, **general**, is the default set of download options. The second item, **classic**, are the download options that apply in the **Classic Mode** tab (see `6.22 Classic Mode`_).
Download options are saved in the Tartube database, so if you switch databases (see `6.20.2 Multiple databases`_), a different selection of download options will apply. If you want to move a set of download options from one database to another, you can **Export** them, then switch databases, then **Import** them.
@ -829,7 +831,7 @@ You can create as many different custom downloads as you like.
If you use custom downloads a lot, you can add some extra buttons to the **Videos** tab.
- Click **Edit > System preferences... > Windows > Main window**
- Click **Show a 'Custom download all' button in the Videos Tab** to select it
- Click **Show a 'Custom download all' button in the Videos tab** to select it
.. image:: screenshots/example21.png
:alt: The option custom download button
@ -999,7 +1001,7 @@ If new videos are later added to the channel, playlist or folder, they will auto
**Tartube** can download videos from several channels and/or playlists into a single directory (folder) on your computer's filesystem. There are four situations in which this might be useful:
- A channel has several playlists. You have added both the channel and its playlists to **Tartube**'s database, but you don't want to download duplicate videos
- A creator releases their videos on **BitChute** as well as on **YouTube**. You have added both channels, but you don't want to download duplicate videos
- A creator releases their videos on **Odysee** as well as on **YouTube**. You have added both channels, but you don't want to download duplicate videos
- You don't care about keeping videos in separate directories/folders on your filesystem. You just want to download all videos to one location
- A separate application will process the videos, after Tartube has downloaded them
@ -1020,15 +1022,15 @@ The solution is to tell **Tartube** to store all the videos from the channel and
6.17.2 Combining channels from different websites
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
A creator might release their videos on **YouTube**, but also on a site like **BitChute**. Sometimes they will only release a particular video on **BitChute**.
A creator might release their videos on **YouTube**, but also on a site like **Odysee**. Sometimes they will only release a particular video on **Odysee**.
You can add both channels in the normal way but, if you do, **Tartube** will download many videos twice.
The solution is to tell **Tartube** to store videos from both channels in a single location. In that way, you can still see a list of videos in each channel, but duplicate videos are not actually downloaded.
- Click **Media > Add channel**..., and then enter the **YouTube** channel's details
- Click **Media > Add channel**..., and then enter the **BitChute** channel's details
- Right-click the **BitChute** channel and select **Channel actions > Set download destination...**
- Click **Media > Add channel**..., and then enter the **Odysee** channel's details
- Right-click the **Odysee** channel and select **Channel actions > Set download destination...**
- In the dialogue window, click **Use a different location**, select the name of the **YouTube** channel, then click the **OK** button
It doesn't matter which of the two channels you use as the download destination. There is also no limit to the number of parallel channels, so if a creator uploads videos to a dozen different websites, you can add them all.
@ -1223,10 +1225,10 @@ Some websites, such as **YouTube**, allow you to download the audio (in **.m4a**
**Tartube** compiles a database of the videos, channels and playlists it has downloaded.
If you want something simpler, then click the **Classic Mode** Tab, which has an interface that looks just like `youtube-dl-gui <https://mrs0m30n3.github.io/youtube-dl-gui/>`__.
If you want something simpler, then click the **Classic Mode** tab, which has an interface that looks just like `youtube-dl-gui <https://mrs0m30n3.github.io/youtube-dl-gui/>`__.
.. image:: screenshots/example25.png
:alt: The Classic Mode Tab
:alt: The Classic Mode tab
- Copy and paste the links (URLs) of videos, channels and/or playlists into the box at the top
- Click the **+** button to select a destination. All the videos are downloaded to this location
@ -1246,7 +1248,7 @@ Because the videos aren't in a database, you can move them anywhere you want (on
If you *only* use this tab, you can tell **Tartube** to open it automatically.
- Click **Edit > System preferences... > Windows > Main window**
- Select **When Tartube starts, automatically open the Classic Mode Tab**
- Select **When Tartube starts, automatically open the Classic Mode tab**
If you don't want **Tartube** to forget URLs when it restarts, you can do this:
@ -1639,6 +1641,39 @@ If a youtube-dl fork is still compatible with the original, then **Tartube** can
- Click **OK** to close the preferences window
- Now click **Operations > Update youtube-dl**, which will download (or update) the fork on your system
6.29 Video Comments
-------------------
**yt-dlp** can retrieve a video's comments (**youtube-dl** cannot, currently).
The comments are written to the video's metadata (**.info.json**) file. **Tartube** can also store a copy of the comments in its database.
Fetching comments will increase the length of a download, perhaps by a lot. Adding comments to Tartube's database may increase its size *dramatically*, meaning that on startup, the database takes much longer to load.
This is how to enable comment fetching.
- Click **Edit > System preferences... > Downloaders > Forks**, and make sure **yt-dlp** is selected
- In the same window, click the tab **Operations > Comments**
- Select **When checking/downloading videos, store comments in the metadata file**
- If you like, you can select **Also store comments in the Tartube database**
By default, the metadata file isn't kept permanently. If you want a permanent archive of video comments, you should do this:
- Click **Edit > General download options... > Files > Write/move files**
- Make sure **Write video's metadata to an .info.json file** is selected
- Now click the tab **Keep files**
- Make sure the two **Keep the metadata file** buttons are selected
Comments can be loaded from the metadata file into the Tartube database at any time.
- Right-click a video, and select **Show video > Properties... > Comments**
- If the metadata file exists, you can click the button **Update from the metadata file**
Alternatively, you can update the entire database at once. (This may take a long time.)
- Click **Edit > System preferences... > Files > Update**
- Click the button **Extract comments for all videos**
7 Frequently-Asked Questions
============================
@ -1731,7 +1766,7 @@ If stability is a problem, you can disable some minor cosmetic features. **Tartu
Another option is to reduce the number of simultaneous downloads. (On crash-prone systems, two simultaneous downloads seems to be safe, but four is rather less safe.)
- In the main window, click the **Progress** Tab
- In the main window, click the **Progress** tab
- At the bottom of the tab, click the **Max downloads** checkbutton to select it, and reduce the number of simultaneous downloads to 1 or 2
- (It's not necessary to reduce the download speed; this has no effect on stability)
@ -1761,7 +1796,7 @@ Because most people don't like typing, **Tartube** offers a shortcut.
- In the dialogue window, enter the link (URL) to the video
- You can add more **youtube-dl** download options, if you want. See `here <https://github.com/ytdl-org/youtube-dl/>`__ for a complete list of them
- Click the **OK** button to close the window and begin the test
- Click the **Output** Tab to watch the test as it progresses
- Click the **Output** tab to watch the test as it progresses
- When the test is finished, a temporary directory (folder) opens, containing anything that **youtube-dl** was able to download
7.5 Downloads never finish
@ -1834,7 +1869,7 @@ You can drastically reduce the time this takes by telling **Tartube** to stop ch
This works well on sites like YouTube, which send information about videos in the order they were uploaded, newest first. We can't guarantee it will work on every site.
- Click **Edit > System preferences... > Operations > Performance**
- Select the checkbox **Stop checking/downloading a channel/playlist when it starts sending vidoes you already have**
- Select the checkbox **Stop checking/downloading a channel/playlist when it starts sending videos you already have**
- In the **Stop after this many videos (when checking)** box, enter the value 3
- In the **Stop after this many videos (when downloading)** box, enter the value 3
- Click **OK** to close the window

View File

@ -1 +1 @@
2.3.306
2.3.321

BIN
icons/small/comment.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 617 B

BIN
icons/small/favourite.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 685 B

BIN
icons/small/likes.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 659 B

View File

Before

Width:  |  Height:  |  Size: 871 B

After

Width:  |  Height:  |  Size: 871 B

View File

Before

Width:  |  Height:  |  Size: 812 B

After

Width:  |  Height:  |  Size: 812 B

BIN
icons/small/uploader.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 593 B

View File

@ -372,7 +372,7 @@ msgid "Operation complete"
msgstr ""
#: .././mainapp.py:10713
msgid "Click the Output Tab to see the results"
msgid "Click the Output tab to see the results"
msgstr ""
#: .././mainapp.py:10726
@ -3792,11 +3792,11 @@ msgid "Usage"
msgstr ""
#: .././config.py:2859
msgid "Applies everywhere except the Classic Mode Tab"
msgid "Applies everywhere except the Classic Mode tab"
msgstr ""
#: .././config.py:2862
msgid "Applies to the Classic Mode Tab"
msgid "Applies to the Classic Mode tab"
msgstr ""
#: .././config.py:2864
@ -3929,7 +3929,7 @@ msgid "All channels, playlists and folders"
msgstr ""
#: .././config.py:3931
msgid "Downloads in the Classic Mode Tab"
msgid "Downloads in the Classic Mode tab"
msgstr ""
#: .././config.py:3933
@ -4616,7 +4616,7 @@ msgid "Download Options"
msgstr ""
#: .././config.py:5813
msgid "Number of fragments of a dash/hls video to download concurrently"
msgid "Number of fragments of a DASH/HLS video to download concurrently"
msgstr ""
#: .././config.py:5828
@ -6506,19 +6506,19 @@ msgid "Show errors/warnings in tooltips (but not in the Videos tab)"
msgstr ""
#: .././config.py:17831
msgid "Disable the download buttons in the toolbar and the Videos Tab"
msgid "Disable the download buttons in the toolbar and the Videos tab"
msgstr ""
#: .././config.py:17841
msgid "Show a 'Custom download all' button in the Videos Tab"
msgid "Show a 'Custom download all' button in the Videos tab"
msgstr ""
#: .././config.py:17851
msgid "In the Progress Tab, hide finished videos / channels / playlists"
msgid "In the Progress tab, hide finished videos / channels / playlists"
msgstr ""
#: .././config.py:17860
msgid "In the Progress Tab, show results in reverse order"
msgid "In the Progress tab, show results in reverse order"
msgstr ""
#: .././config.py:17868
@ -6527,16 +6527,16 @@ msgstr ""
#: .././config.py:17882
msgid ""
"In the Classic Mode Tab, when adding URLs, remove duplicates rather than "
"In the Classic Mode tab, when adding URLs, remove duplicates rather than "
"retaining them"
msgstr ""
#: .././config.py:17896
msgid "In the Errors/Warnings Tab, don't reset the tab text when it is clicked"
msgid "In the Errors/Warnings tab, don't reset the tab text when it is clicked"
msgstr ""
#: .././config.py:17919
msgid "Video Index (left side of the Videos Tab)"
msgid "Video Index (left side of the Videos tab)"
msgstr ""
#: .././config.py:17925
@ -6559,7 +6559,7 @@ msgid "Expand the whole tree, not just the level beneath the clicked folder"
msgstr ""
#: .././config.py:17978
msgid "Video Catalogue (right side of the Videos Tab)"
msgid "Video Catalogue (right side of the Videos tab)"
msgstr ""
#: .././config.py:17985
@ -7465,19 +7465,19 @@ msgid "O_utput"
msgstr ""
#: .././config.py:21628
msgid "_Output Tab"
msgid "_Output tab"
msgstr ""
#: .././config.py:21636
msgid "Output Tab preferences"
msgid "Output tab preferences"
msgstr ""
#: .././config.py:21641
msgid "Display downloader system commands in the Output Tab"
msgid "Display downloader system commands in the Output tab"
msgstr ""
#: .././config.py:21650
msgid "Display output from downloader's STDOUT in the Output Tab"
msgid "Display output from downloader's STDOUT in the Output tab"
msgstr ""
#: .././config.py:21659 .././config.py:21821
@ -7489,15 +7489,15 @@ msgid "...but don't write each video's download progress"
msgstr ""
#: .././config.py:21689
msgid "Display output from downloader's STDERR in the Output Tab"
msgid "Display output from downloader's STDERR in the Output tab"
msgstr ""
#: .././config.py:21698
msgid "Limit the size of Output Tab pages to"
msgid "Limit the size of Output tab pages to"
msgstr ""
#: .././config.py:21719
msgid "Empty pages in the Output Tab at the start of every operation"
msgid "Empty pages in the Output tab at the start of every operation"
msgstr ""
#: .././config.py:21729
@ -7510,7 +7510,7 @@ msgid "During update/info operations, automatically switch to the Output tab"
msgstr ""
#: .././config.py:21752
msgid "During a refresh operation, show all matching videos in the Output Tab"
msgid "During a refresh operation, show all matching videos in the Output tab"
msgstr ""
#: .././config.py:21763
@ -7543,7 +7543,7 @@ msgstr ""
#: .././config.py:21878
msgid ""
"Special preferences (applies to both the Output Tab and the terminal window)"
"Special preferences (applies to both the Output tab and the terminal window)"
msgstr ""
#: .././config.py:21885
@ -7814,7 +7814,7 @@ msgstr ""
msgid "FAILED: Can't create a temporary folder for video clips"
msgstr ""
#. This object creates more messages for the Output Tab and/or terminal,
#. This object creates more messages for the Output tab and/or terminal,
#. than downloads.VideoDownloader would do, as the output generated by
#. Youtube Stream Capture is not easy for the user to interpret
#: .././downloads.py:7578
@ -8517,7 +8517,7 @@ msgstr ""
msgid "FFmpeg installation did not start"
msgstr ""
#. Show a confirmation in the the Output Tab (or wizard window textview)
#. Show a confirmation in the the Output tab (or wizard window textview)
#: .././updates.py:289 .././updates.py:509
msgid "Update operation finished"
msgstr ""

View File

@ -390,7 +390,7 @@ msgid "Operation complete"
msgstr "작업 성공"
#: .././mainapp.py:10713
msgid "Click the Output Tab to see the results"
msgid "Click the Output tab to see the results"
msgstr "결과를 보려면 출력 탭을 클릭하세요"
#: .././mainapp.py:10726
@ -3854,11 +3854,11 @@ msgid "Usage"
msgstr ""
#: .././config.py:2859
msgid "Applies everywhere except the Classic Mode Tab"
msgid "Applies everywhere except the Classic Mode tab"
msgstr ""
#: .././config.py:2862
msgid "Applies to the Classic Mode Tab"
msgid "Applies to the Classic Mode tab"
msgstr ""
#: .././config.py:2864
@ -3991,7 +3991,7 @@ msgid "All channels, playlists and folders"
msgstr "모든 채널, 플레이리스트, 폴더"
#: .././config.py:3931
msgid "Downloads in the Classic Mode Tab"
msgid "Downloads in the Classic Mode tab"
msgstr "클래식 모드 탭의 다운로드"
#: .././config.py:3933
@ -4681,7 +4681,7 @@ msgid "Download Options"
msgstr ""
#: .././config.py:5813
msgid "Number of fragments of a dash/hls video to download concurrently"
msgid "Number of fragments of a DASH/HLS video to download concurrently"
msgstr ""
#: .././config.py:5828
@ -6616,19 +6616,19 @@ msgid "Show errors/warnings in tooltips (but not in the Videos tab)"
msgstr "툴팁에 오류/경고 표시 (비디오 탭에선 아님)"
#: .././config.py:17831
msgid "Disable the download buttons in the toolbar and the Videos Tab"
msgid "Disable the download buttons in the toolbar and the Videos tab"
msgstr ""
#: .././config.py:17841
msgid "Show a 'Custom download all' button in the Videos Tab"
msgid "Show a 'Custom download all' button in the Videos tab"
msgstr ""
#: .././config.py:17851
msgid "In the Progress Tab, hide finished videos / channels / playlists"
msgid "In the Progress tab, hide finished videos / channels / playlists"
msgstr "작업 탭에서 완료한 비디오 / 채널 / 플레이리스트 숨기기"
#: .././config.py:17860
msgid "In the Progress Tab, show results in reverse order"
msgid "In the Progress tab, show results in reverse order"
msgstr "작업 탭에서, 결과를 반대로 보여주기"
#: .././config.py:17868
@ -6637,16 +6637,16 @@ msgstr "Tartube가 시작하면, 자동으로 클래식 모드 탭으로 열기"
#: .././config.py:17882
msgid ""
"In the Classic Mode Tab, when adding URLs, remove duplicates rather than "
"In the Classic Mode tab, when adding URLs, remove duplicates rather than "
"retaining them"
msgstr "클래식 모드 탭에서, URL을 추가할 때, 중복 항목을 유지하지 않고 제거"
#: .././config.py:17896
msgid "In the Errors/Warnings Tab, don't reset the tab text when it is clicked"
msgid "In the Errors/Warnings tab, don't reset the tab text when it is clicked"
msgstr "오류/경고 탭에서, 탭 텍스트를 클릭 시 재설정하지 않음"
#: .././config.py:17919
msgid "Video Index (left side of the Videos Tab)"
msgid "Video Index (left side of the Videos tab)"
msgstr "비디오 색인 (비디오 탭의 왼쪽)"
#: .././config.py:17925
@ -6671,7 +6671,7 @@ msgid "Expand the whole tree, not just the level beneath the clicked folder"
msgstr "클릭한 폴더 아래뿐 아니라 전체 트리 확장하기"
#: .././config.py:17978
msgid "Video Catalogue (right side of the Videos Tab)"
msgid "Video Catalogue (right side of the Videos tab)"
msgstr "비디오 카탈로그 (비디오 탭의 오른쪽)"
#: .././config.py:17985
@ -7590,19 +7590,19 @@ msgid "O_utput"
msgstr "_출력"
#: .././config.py:21628
msgid "_Output Tab"
msgid "_Output tab"
msgstr "_출력 탭"
#: .././config.py:21636
msgid "Output Tab preferences"
msgid "Output tab preferences"
msgstr "출력 탭 환경 설정"
#: .././config.py:21641
msgid "Display downloader system commands in the Output Tab"
msgid "Display downloader system commands in the Output tab"
msgstr "출력 탭에 다운로드 시스템 커맨드 표시"
#: .././config.py:21650
msgid "Display output from downloader's STDOUT in the Output Tab"
msgid "Display output from downloader's STDOUT in the Output tab"
msgstr "출력 탭에 다운로더의 STDOUT 출력 표시"
#: .././config.py:21659 .././config.py:21821
@ -7614,15 +7614,15 @@ msgid "...but don't write each video's download progress"
msgstr "...하지만 각각의 비디오 다운로드 진행 상황은 쓰지 않기"
#: .././config.py:21689
msgid "Display output from downloader's STDERR in the Output Tab"
msgid "Display output from downloader's STDERR in the Output tab"
msgstr "출력 탭에 다운로더의 STDERR 출력 표시"
#: .././config.py:21698
msgid "Limit the size of Output Tab pages to"
msgid "Limit the size of Output tab pages to"
msgstr "출력 탭의 페이지 사이즈 제한하기"
#: .././config.py:21719
msgid "Empty pages in the Output Tab at the start of every operation"
msgid "Empty pages in the Output tab at the start of every operation"
msgstr "모든 작업을 시작 할 때 출력 탭 비우기"
#: .././config.py:21729
@ -7635,7 +7635,7 @@ msgid "During update/info operations, automatically switch to the Output tab"
msgstr ""
#: .././config.py:21752
msgid "During a refresh operation, show all matching videos in the Output Tab"
msgid "During a refresh operation, show all matching videos in the Output tab"
msgstr "새로고침 작업 중, 자동으로 출력 탭으로 전환"
#: .././config.py:21763
@ -7668,7 +7668,7 @@ msgstr "_둘 다"
#: .././config.py:21878
msgid ""
"Special preferences (applies to both the Output Tab and the terminal window)"
"Special preferences (applies to both the Output tab and the terminal window)"
msgstr "특별한 환경 설정 (출력 탭과 터미널 윈도우에 둘다 적용됨)"
#: .././config.py:21885
@ -7940,7 +7940,7 @@ msgstr ""
msgid "FAILED: Can't create a temporary folder for video clips"
msgstr ""
#. This object creates more messages for the Output Tab and/or terminal,
#. This object creates more messages for the Output tab and/or terminal,
#. than downloads.VideoDownloader would do, as the output generated by
#. Youtube Stream Capture is not easy for the user to interpret
#: .././downloads.py:7578
@ -8649,7 +8649,7 @@ msgstr "업데이트 작업 시작, FFmpeg 설치중"
msgid "FFmpeg installation did not start"
msgstr "FFmpeg 설치가 시작되지 않음"
#. Show a confirmation in the the Output Tab (or wizard window textview)
#. Show a confirmation in the the Output tab (or wizard window textview)
#: .././updates.py:289 .././updates.py:509
msgid "Update operation finished"
msgstr "업데이트 작업이 끝남"
@ -8757,7 +8757,7 @@ msgstr "업데이트가 시작하지 않음"
#~ msgid "_Temporary folders"
#~ msgstr "_임시 폴더"
#~ msgid "Disable the 'Download all' buttons in the toolbar and the Videos Tab"
#~ msgid "Disable the 'Download all' buttons in the toolbar and the Videos tab"
#~ msgstr "툴바와 비디오 탭에서 '모두 다운로드' 버튼 비활성화"
#~ msgid "_System tray"

View File

@ -420,7 +420,7 @@ msgid "Operation complete"
msgstr "Handeling voltooid"
#: .././mainapp.py:10713
msgid "Click the Output Tab to see the results"
msgid "Click the Output tab to see the results"
msgstr "Klik op het tabblad 'Uitvoer' om de resultaten te bekijken"
#: .././mainapp.py:10726
@ -3904,11 +3904,11 @@ msgid "Usage"
msgstr ""
#: .././config.py:2859
msgid "Applies everywhere except the Classic Mode Tab"
msgid "Applies everywhere except the Classic Mode tab"
msgstr ""
#: .././config.py:2862
msgid "Applies to the Classic Mode Tab"
msgid "Applies to the Classic Mode tab"
msgstr ""
#: .././config.py:2864
@ -4044,7 +4044,7 @@ msgid "All channels, playlists and folders"
msgstr "Alle kanalen, afspeellijsten en mappen"
#: .././config.py:3931
msgid "Downloads in the Classic Mode Tab"
msgid "Downloads in the Classic Mode tab"
msgstr "Downloads in de klassieke modus"
#: .././config.py:3933
@ -4743,7 +4743,7 @@ msgid "Download Options"
msgstr ""
#: .././config.py:5813
msgid "Number of fragments of a dash/hls video to download concurrently"
msgid "Number of fragments of a DASH/HLS video to download concurrently"
msgstr ""
#: .././config.py:5828
@ -6724,20 +6724,20 @@ msgstr ""
"'Video's')"
#: .././config.py:17831
msgid "Disable the download buttons in the toolbar and the Videos Tab"
msgid "Disable the download buttons in the toolbar and the Videos tab"
msgstr ""
#: .././config.py:17841
msgid "Show a 'Custom download all' button in the Videos Tab"
msgid "Show a 'Custom download all' button in the Videos tab"
msgstr ""
#: .././config.py:17851
msgid "In the Progress Tab, hide finished videos / channels / playlists"
msgid "In the Progress tab, hide finished videos / channels / playlists"
msgstr ""
"Afgeronde video's, kanalen en afspeellijsten verbergen op voortgangstabblad"
#: .././config.py:17860
msgid "In the Progress Tab, show results in reverse order"
msgid "In the Progress tab, show results in reverse order"
msgstr "Resultaten in omgekeerde volgorde tonen op voortgangstabblad"
#: .././config.py:17868
@ -6746,18 +6746,18 @@ msgstr "Automatisch opstarten in klassieke modus"
#: .././config.py:17882
msgid ""
"In the Classic Mode Tab, when adding URLs, remove duplicates rather than "
"In the Classic Mode tab, when adding URLs, remove duplicates rather than "
"retaining them"
msgstr "Duplicaten verwijderen tijdens toevoegen van url's in klassieke modus"
#: .././config.py:17896
msgid "In the Errors/Warnings Tab, don't reset the tab text when it is clicked"
msgid "In the Errors/Warnings tab, don't reset the tab text when it is clicked"
msgstr ""
"Herstel de tabbladtekst niet na aanklikken op het tabblad 'Fouten/"
"Waarschuwingen'"
#: .././config.py:17919
msgid "Video Index (left side of the Videos Tab)"
msgid "Video Index (left side of the Videos tab)"
msgstr "Video-index (links van het tabblad 'Video's')"
#: .././config.py:17925
@ -6782,7 +6782,7 @@ msgid "Expand the whole tree, not just the level beneath the clicked folder"
msgstr "Boomstructuur volledig uitklappen na aanklikken"
#: .././config.py:17978
msgid "Video Catalogue (right side of the Videos Tab)"
msgid "Video Catalogue (right side of the Videos tab)"
msgstr "Videocatalogus (rechts van het tabblad 'Video's')"
#: .././config.py:17985
@ -7715,19 +7715,19 @@ msgid "O_utput"
msgstr "_Uitvoer"
#: .././config.py:21628
msgid "_Output Tab"
msgid "_Output tab"
msgstr "_Uitvoertabblad"
#: .././config.py:21636
msgid "Output Tab preferences"
msgid "Output tab preferences"
msgstr "Instellingen omtrent uitvoertabblad"
#: .././config.py:21641
msgid "Display downloader system commands in the Output Tab"
msgid "Display downloader system commands in the Output tab"
msgstr "Systeemopdrachten van downloader tonen op tabblad 'Uitvoer'"
#: .././config.py:21650
msgid "Display output from downloader's STDOUT in the Output Tab"
msgid "Display output from downloader's STDOUT in the Output tab"
msgstr "STDOUT-uitvoer van downloader tonen op tabblad 'Uitvoer'"
#: .././config.py:21659 .././config.py:21821
@ -7739,15 +7739,15 @@ msgid "...but don't write each video's download progress"
msgstr "...maar schrijf geen downloadvoortgang van elke video weg"
#: .././config.py:21689
msgid "Display output from downloader's STDERR in the Output Tab"
msgid "Display output from downloader's STDERR in the Output tab"
msgstr "STDERR-uitvoer van downloader tonen op tabblad 'Uitvoer'"
#: .././config.py:21698
msgid "Limit the size of Output Tab pages to"
msgid "Limit the size of Output tab pages to"
msgstr "Aantal pagina's op tabblad 'Uitvoer' beperken tot"
#: .././config.py:21719
msgid "Empty pages in the Output Tab at the start of every operation"
msgid "Empty pages in the Output tab at the start of every operation"
msgstr "Pagina's op tabblad 'Uitvoer' legen na elke actie"
#: .././config.py:21729
@ -7762,7 +7762,7 @@ msgid "During update/info operations, automatically switch to the Output tab"
msgstr ""
#: .././config.py:21752
msgid "During a refresh operation, show all matching videos in the Output Tab"
msgid "During a refresh operation, show all matching videos in the Output tab"
msgstr ""
"Alle overeenkomende video's tonen op tabblad 'Uitvoer' tijdens bijwerken"
@ -7796,7 +7796,7 @@ msgstr "_Beide"
#: .././config.py:21878
msgid ""
"Special preferences (applies to both the Output Tab and the terminal window)"
"Special preferences (applies to both the Output tab and the terminal window)"
msgstr ""
"Speciale instellingen (van toepassing op het tabblad 'Uitvoer' en het "
"terminalvenster)"
@ -8072,7 +8072,7 @@ msgstr ""
msgid "FAILED: Can't create a temporary folder for video clips"
msgstr ""
#. This object creates more messages for the Output Tab and/or terminal,
#. This object creates more messages for the Output tab and/or terminal,
#. than downloads.VideoDownloader would do, as the output generated by
#. Youtube Stream Capture is not easy for the user to interpret
#: .././downloads.py:7578
@ -8789,7 +8789,7 @@ msgstr "Bezig met starten van bijwerkactie om FFmpeg te installeren"
msgid "FFmpeg installation did not start"
msgstr "De FFmpeg-installatie kan niet worden gestart"
#. Show a confirmation in the the Output Tab (or wizard window textview)
#. Show a confirmation in the the Output tab (or wizard window textview)
#: .././updates.py:289 .././updates.py:509
msgid "Update operation finished"
msgstr "Bijwerkactie voltooid"
@ -8897,7 +8897,7 @@ msgstr "Het bijwerken is niet gestart"
#~ msgid "_Temporary folders"
#~ msgstr "_Tijdelijke mappen"
#~ msgid "Disable the 'Download all' buttons in the toolbar and the Videos Tab"
#~ msgid "Disable the 'Download all' buttons in the toolbar and the Videos tab"
#~ msgstr ""
#~ "'Alles downloaden'-knoppen op werkbalk en tabblad 'Video's' uitschakelen"

View File

@ -1,4 +1,4 @@
# Tartube v2.3.306 installer script for MS Windows
# Tartube v2.3.321 installer script for MS Windows
#
# Copyright (C) 2019-2021 A S Lewis
#
@ -249,7 +249,7 @@
;Name and file
Name "Tartube"
OutFile "install-tartube-2.3.306-32bit.exe"
OutFile "install-tartube-2.3.321-32bit.exe"
;Default installation folder
InstallDir "$LOCALAPPDATA\Tartube"
@ -352,7 +352,7 @@ Section "Tartube" SecClient
# "Publisher" "A S Lewis"
# WriteRegStr HKLM \
# "Software\Microsoft\Windows\CurrentVersion\Uninstall\Tartube" \
# "DisplayVersion" "2.3.306"
# "DisplayVersion" "2.3.321"
# Create uninstaller
WriteUninstaller "$INSTDIR\Uninstall.exe"

View File

@ -1,4 +1,4 @@
# Tartube v2.3.306 installer script for MS Windows
# Tartube v2.3.321 installer script for MS Windows
#
# Copyright (C) 2019-2021 A S Lewis
#
@ -249,7 +249,7 @@
;Name and file
Name "Tartube"
OutFile "install-tartube-2.3.306-64bit.exe"
OutFile "install-tartube-2.3.321-64bit.exe"
;Default installation folder
InstallDir "$LOCALAPPDATA\Tartube"
@ -352,7 +352,7 @@ Section "Tartube" SecClient
# "Publisher" "A S Lewis"
# WriteRegStr HKLM \
# "Software\Microsoft\Windows\CurrentVersion\Uninstall\Tartube" \
# "DisplayVersion" "2.3.306"
# "DisplayVersion" "2.3.321"
# Create uninstaller
WriteUninstaller "$INSTDIR\Uninstall.exe"

View File

@ -42,8 +42,8 @@ import mainapp
# 'Global' variables
__packagename__ = 'tartube'
__version__ = '2.3.306'
__date__ = '3 Aug 2021'
__version__ = '2.3.321'
__date__ = '5 Aug 2021'
__copyright__ = 'Copyright \xa9 2019-2021 A S Lewis'
__license__ = """
Copyright \xa9 2019-2021 A S Lewis.

View File

@ -42,8 +42,8 @@ import mainapp
# 'Global' variables
__packagename__ = 'tartube'
__version__ = '2.3.306'
__date__ = '3 Aug 2021'
__version__ = '2.3.321'
__date__ = '5 Aug 2021'
__copyright__ = 'Copyright \xa9 2019-2021 A S Lewis'
__license__ = """
Copyright \xa9 2019-2021 A S Lewis.

View File

@ -42,8 +42,8 @@ import mainapp
# 'Global' variables
__packagename__ = 'tartube'
__version__ = '2.3.306'
__date__ = '3 Aug 2021'
__version__ = '2.3.321'
__date__ = '5 Aug 2021'
__copyright__ = 'Copyright \xa9 2019-2021 A S Lewis'
__license__ = """
Copyright \xa9 2019-2021 A S Lewis.

View File

@ -1,4 +1,4 @@
.TH man 1 "3 Aug 2021" "2.3.306" "tartube man page"
.TH man 1 "5 Aug 2021" "2.3.321" "tartube man page"
.SH NAME
tartube \- GUI front-end for youtube-dl
.SH SYNOPSIS

View File

@ -1,6 +1,6 @@
[Desktop Entry]
Name=Tartube
Version=2.3.306
Version=2.3.321
Exec=tartube
Icon=tartube
Type=Application

Binary file not shown.

Before

Width:  |  Height:  |  Size: 51 KiB

After

Width:  |  Height:  |  Size: 54 KiB

View File

@ -182,7 +182,7 @@ for path in glob.glob('sounds/*'):
# Setup
setuptools.setup(
name='tartube',
version='2.3.306',
version='2.3.321',
description='GUI front-end for youtube-dl',
long_description=long_description,
long_description_content_type='text/plain',

File diff suppressed because it is too large Load Diff

View File

@ -48,7 +48,7 @@ class DialogueManager(threading.Thread):
Args:
app_obj: The mainapp.TartubeApp object
app_obj (mainapp.TartubeApp): The main application
main_win_obj (mainwin.MainWin): The main window
@ -220,9 +220,9 @@ class DialogueManager(threading.Thread):
The parent window for the dialogue window. If None, the main
window is used as the parent window
action (str or None): The type of fille chooser
to create: 'open' to set a file for opening, 'save' to save a
file, or 'folder' to select a folder
action (str or None): The type of fille chooser to create: 'open'
to set a file for opening, 'save' to save a file, or 'folder'
to select a folder
file_path (str or None): The file path to suggest to the user. If
not specified, then the file chooser is opened in Tartube's

View File

@ -106,7 +106,7 @@ class DownloadManager(threading.Thread):
is the same as a 'sim' operation, except that it is always followed
by a 'custom_real' operation)
For downloads launched from the Classic Mode Tab, 'classic_real'
For downloads launched from the Classic Mode tab, 'classic_real'
for an ordinary download, or 'classic_custom' for a custom
download. A 'classic_custom' operation is always preceded by a
'classic_sim' operation (which is the same as a 'sim' operation,
@ -174,7 +174,7 @@ class DownloadManager(threading.Thread):
# operation is sometimes preceded by a 'custom_sim' operation (which
# is the same as a 'sim' operation, except that it is always followed
# by a 'custom_real' operation)
# For downloads launched from the Classic Mode Tab, 'classic_real' for
# For downloads launched from the Classic Mode tab, 'classic_real' for
# an ordinary download, or 'classic_custom' for a custom download. A
# 'classic_custom' operation is always preceded by a 'classic_sim'
# operation (which is the same as a 'sim' operation, except that it
@ -327,7 +327,7 @@ class DownloadManager(threading.Thread):
)
# (Monitor changes to the number of workers, and number of available
# workers, so that we can display a running total in the Output Tab's
# workers, so that we can display a running total in the Output tab's
# summary page)
local_worker_available_count = 0
local_worker_total_count = 0
@ -337,7 +337,7 @@ class DownloadManager(threading.Thread):
# self.stop_download_operation()
while self.running_flag:
# Send a message to the Output Tab's summary page, if required.
# Send a message to the Output tab's summary page, if required.
# The number of workers shown doesn't include those dedicated to
# broadcasting livestreams
available_count = 0
@ -434,7 +434,7 @@ class DownloadManager(threading.Thread):
if not self.current_item_obj:
if self.check_workers_all_finished():
# Send a message to the Output Tab's summary page
# Send a message to the Output tab's summary page
self.app_obj.main_win_obj.output_tab_write_stdout(
0,
manager_string + _('All threads finished'),
@ -458,7 +458,7 @@ class DownloadManager(threading.Thread):
# Otherwise, initialise the worker's IVs for the next job
elif worker_obj:
# Send a message to the Output Tab's summary page
# Send a message to the Output tab's summary page
self.app_obj.main_win_obj.output_tab_write_stdout(
0,
_('Thread #') + str(worker_obj.worker_id) \
@ -489,7 +489,7 @@ class DownloadManager(threading.Thread):
time.sleep(self.sleep_time)
# Download operation complete (or has been stopped). Send messages to
# the Output Tab's summary page
# the Output tab's summary page
self.app_obj.main_win_obj.output_tab_write_stdout(
0,
manager_string + _('Downloads complete (or stopped)'),
@ -537,7 +537,7 @@ class DownloadManager(threading.Thread):
self.app_obj.main_win_obj.classic_mode_tab_display_dl_stats,
)
# Tell the Output Tab to display any remaining messages immediately
# Tell the Output tab to display any remaining messages immediately
GObject.timeout_add(
0,
self.app_obj.main_win_obj.output_tab_update_pages,
@ -572,7 +572,7 @@ class DownloadManager(threading.Thread):
else:
# For download operations launched from the Classic Mode Tab, we
# For download operations launched from the Classic Mode tab, we
# don't need to wait at all
GObject.timeout_add(
0,
@ -610,7 +610,7 @@ class DownloadManager(threading.Thread):
Returns:
True if alternative limits apply, False if not.
True if alternative limits apply, False if not
"""
@ -686,7 +686,7 @@ class DownloadManager(threading.Thread):
Returns:
True if the alternative limits apply today, False if not.
True if the alternative limits apply today, False if not
"""
@ -889,7 +889,7 @@ class DownloadManager(threading.Thread):
# Bypass the worker limit to create an additional worker, to be used
# only for broadcasting livestreams
self.worker_list.append(DownloadWorker(self, True))
# Create an additional page in the main window's Output Tab, if
# Create an additional page in the main window's Output tab, if
# required
self.app_obj.main_win_obj.output_tab_setup_pages()
@ -909,7 +909,7 @@ class DownloadManager(threading.Thread):
Returns:
The first available downloads.DownloadWorker, or None if there are
no available workers.
no available workers
"""
@ -1058,7 +1058,7 @@ class DownloadManager(threading.Thread):
a new video.
Can also be called by .confirm_old_video() when downloading from the
Classic Mode Tab.
Classic Mode tab.
Furthermore, called by ClipDownloader.do_download() when all clips for
a video have been extracted, at least one of them successfully.
@ -1255,7 +1255,7 @@ class DownloadWorker(threading.Thread):
# IV list - other
# ---------------
# A number identifying this worker, matching the number of the page
# in the Output Tab (so the first worker created is #1)
# in the Output tab (so the first worker created is #1)
self.worker_id = len(download_manager_obj.worker_list) + 1
# The time (in seconds) between iterations of the loop in self.run()
@ -1377,7 +1377,7 @@ class DownloadWorker(threading.Thread):
else:
self.run_video_downloader(media_data_obj)
# Send a message to the Output Tab's summary page
# Send a message to the Output tab's summary page
app_obj.main_win_obj.output_tab_write_stdout(
0,
_('Thread #') + str(self.worker_id) \
@ -1388,7 +1388,7 @@ class DownloadWorker(threading.Thread):
# This worker is now available for a new job
self.available_flag = True
# Send a message to the Output Tab's summary page
# Send a message to the Output tab's summary page
app_obj.main_win_obj.output_tab_write_stdout(
0,
_('Thread #') + str(self.worker_id) \
@ -1435,7 +1435,7 @@ class DownloadWorker(threading.Thread):
media_data_obj (media.Video, media.Channel, media.Playlist,
media.Folder): The media data object being downloaded. When the
download operation was launched from the Classic Mode Tab, a
download operation was launched from the Classic Mode tab, a
dummy media.Video object
"""
@ -1463,7 +1463,7 @@ class DownloadWorker(threading.Thread):
if first_flag:
first_flag = False
# Send a message to the Output Tab's summary page
# Send a message to the Output tab's summary page
app_obj.main_win_obj.output_tab_write_stdout(
0,
_('Thread #') + str(self.worker_id) \
@ -1535,7 +1535,7 @@ class DownloadWorker(threading.Thread):
and media_data_obj.child_list \
and media_data_obj.rss:
# Send a message to the Output Tab's summary page
# Send a message to the Output tab's summary page
app_obj.main_win_obj.output_tab_write_stdout(
0,
_('Thread #') + str(self.worker_id) \
@ -1557,7 +1557,7 @@ class DownloadWorker(threading.Thread):
media_data_obj (media.Video): The media data object being
downloaded. When the download operation was launched from the
Classic Mode Tab, a dummy media.Video object
Classic Mode tab, a dummy media.Video object
"""
@ -1576,7 +1576,7 @@ class DownloadWorker(threading.Thread):
self.download_item_obj,
)
# Send a message to the Output Tab's summary page
# Send a message to the Output tab's summary page
app_obj.main_win_obj.output_tab_write_stdout(
0,
_('Thread #') + str(self.worker_id) \
@ -1648,7 +1648,7 @@ class DownloadWorker(threading.Thread):
self.download_item_obj,
)
# Send a message to the Output Tab's summary page
# Send a message to the Output tab's summary page
app_obj.main_win_obj.output_tab_write_stdout(
0,
_('Thread #') + str(self.worker_id) \
@ -1970,7 +1970,7 @@ class DownloadList(object):
is the same as a 'sim' operation, except that it is always followed
by a 'custom_real' operation)
For downloads launched from the Classic Mode Tab, 'classic_real'
For downloads launched from the Classic Mode tab, 'classic_real'
for an ordinary download, or 'classic_custom' for a custom
download. A 'classic_custom' operation is always preceded by a
'classic_sim' operation (which is the same as a 'sim' operation,
@ -2029,7 +2029,7 @@ class DownloadList(object):
# operation is sometimes preceded by a 'custom_sim' operation (which
# is the same as a 'sim' operation, except that it is always followed
# by a 'custom_real' operation)
# For downloads launched from the Classic Mode Tab, 'classic_real' for
# For downloads launched from the Classic Mode tab, 'classic_real' for
# an ordinary download, or 'classic_custom' for a custom download. A
# 'classic_custom' operation is always preceded by a 'classic_sim'
# operation (which is the same as a 'sim' operation, except that it
@ -2057,7 +2057,7 @@ class DownloadList(object):
# An ordered list of downloads.DownloadItem objects, one for each
# media.Video, media.Channel, media.Playlist or media.Folder object
# (including dummy media.Video objects used by download operations
# launched from the Classic Mode Tab)
# launched from the Classic Mode tab)
# This list stores each item's .item_id
self.download_item_list = []
# A supplementary list of downloads.DownloadItem objects
@ -2211,12 +2211,12 @@ class DownloadList(object):
# Downloads from the Classic Mode tab
else:
# The download operation was launched from the Classic Mode Tab.
# The download operation was launched from the Classic Mode tab.
# Each URL to be downloaded is represented by a dummy media.Video
# object (one which is not in the media data registry)
main_win_obj = self.app_obj.main_win_obj
# The user may have rearranged rows in the Classic Mode Tab, so
# The user may have rearranged rows in the Classic Mode tab, so
# get a list of (all) dummy media.Videos in the rearranged order
# (It should be safe to assume that the Gtk.Liststore contains
# exactly the same number of rows, as dummy media.Video objects
@ -2667,7 +2667,7 @@ class DownloadList(object):
def create_dummy_item(self, media_data_obj):
"""Called by self.__init__() only, when the download operation was
launched from the Classic Mode Tab (this function is not called
launched from the Classic Mode tab (this function is not called
recursively).
Creates a downloads.DownloadItem object for each dummy media.Video
@ -2723,7 +2723,7 @@ class DownloadList(object):
Returns:
The next downloads.DownloadItem object, or None if there are none
left.
left
"""
@ -2880,12 +2880,12 @@ class DownloadItem(object):
Args:
item_id (int) - The number of downloads.DownloadItem objects created,
item_id (int): The number of downloads.DownloadItem objects created,
used to give each one a unique ID
media_data_obj (media.Video, media.Channel, media.Playlist,
media.Folder): The media data object to be downloaded. When the
download operation was launched from the Classic Mode Tab, a dummy
download operation was launched from the Classic Mode tab, a dummy
media.Video object
scheduled_obj (media.Scheduled): The scheduled download object which
@ -2917,7 +2917,7 @@ class DownloadItem(object):
# IV list - class objects
# -----------------------
# The media data object to be downloaded. When the download operation
# was launched from the Classic Mode Tab, a dummy media.Video object
# was launched from the Classic Mode tab, a dummy media.Video object
self.media_data_obj = media_data_obj
# The scheduled download object which wants to download media_data_obj
# (None if no scheduled download applies in this case)
@ -3098,7 +3098,7 @@ class VideoDownloader(object):
# object, or False if we actually downloading videos (set below)
self.dl_sim_flag = False
# Flag set to True if this download operation was launched from the
# Classic Mode Tab, False if not (set below)
# Classic Mode tab, False if not (set below)
self.dl_classic_flag = False
# Flag set to True by a call from any function to self.stop_soon()
@ -3141,14 +3141,14 @@ class VideoDownloader(object):
# value = the media.Video object created
self.video_check_dict = {}
# The code imported from youtube-dl-gui doesn't recognise a downloaded
# video, if Ffmpeg isn't used to extract it (because Ffmpeg is not
# video, if FFmpeg isn't used to extract it (because FFmpeg is not
# installed, or because the website doesn't support it, or whatever)
# In this situation, youtube-dl's STDOUT messages don't definitively
# establish when it has finished downloading a video
# When a file destination is announced; it is temporarily stored in
# these IVs. When STDOUT receives a message in the form
# [download] 100% of 2.06MiB in 00:02
# ...and the filename isn't one that Ffmpeg would use (e.g.
# ...and the filename isn't one that FFmpeg would use (e.g.
# 'myvideo.f136.mp4' or 'myvideo.f136.m4a', then assume that the
# video has finished downloading
self.temp_path = None
@ -3204,7 +3204,7 @@ class VideoDownloader(object):
# All media data objects can be marked as simulate downloads only
# (except when the download operation was launched from the Classic
# Mode Tab)
# Mode tab)
# The setting applies not just to the media data object, but all of its
# descendants
if not self.download_item_obj.operation_classic_flag:
@ -3270,7 +3270,7 @@ class VideoDownloader(object):
Returns:
The final return code, a value in the range 0-5 (as described
above)
above)
"""
@ -3338,7 +3338,7 @@ class VideoDownloader(object):
divert_mode,
)
# ...display it in the Output Tab (if required)...
# ...display it in the Output tab (if required)...
if app_obj.ytdl_output_system_cmd_flag:
app_obj.main_win_obj.output_tab_write_system_cmd(
self.download_worker_obj.worker_id,
@ -3397,7 +3397,7 @@ class VideoDownloader(object):
# main window can be updated
self.download_worker_obj.data_callback(dl_stat_dict)
# Show output in the Output Tab (if required). For
# Show output in the Output tab (if required). For
# simulated downloads, a message is displayed by
# self.confirm_sim_video() instead
if app_obj.ytdl_output_stdout_flag \
@ -3509,7 +3509,7 @@ class VideoDownloader(object):
self.set_return_code(self.ERROR)
self.download_item_obj.media_data_obj.set_error(stderr)
# Show output in the Output Tab (if required)
# Show output in the Output tab (if required)
if app_obj.ytdl_output_stderr_flag:
app_obj.main_win_obj.output_tab_write_stderr(
self.download_worker_obj.worker_id,
@ -3645,7 +3645,7 @@ class VideoDownloader(object):
utils.debug_time('dld 3032 check_dl_is_correct_type')
# Special case: if the download operation was launched from the
# Classic Mode Tab, there is no need to do anything
# Classic Mode tab, there is no need to do anything
if self.dl_classic_flag:
return True
@ -3837,7 +3837,7 @@ class VideoDownloader(object):
self.download_manager_obj.register_video('new')
# Special case: if the download operation was launched from the
# Classic Mode Tab, then we only need to update the dummy
# Classic Mode tab, then we only need to update the dummy
# media.Video object, and to move/remove description/metadata/
# thumbnail files, as appropriate
elif self.dl_classic_flag:
@ -4016,7 +4016,7 @@ class VideoDownloader(object):
self.download_manager_obj.register_video('old')
# Special case: if the download operation was launched from the
# Classic Mode Tab, then we only need to update the dummy
# Classic Mode tab, then we only need to update the dummy
# media.Video object
elif self.dl_classic_flag:
@ -4243,6 +4243,11 @@ class VideoDownloader(object):
else:
live_flag = False
if 'comments' in json_dict:
comment_list = json_dict['comments']
else:
comment_list = []
# Does an existing media.Video object match this video?
media_data_obj = self.download_item_obj.media_data_obj
video_obj = None
@ -4344,6 +4349,9 @@ class VideoDownloader(object):
app_obj.main_win_obj.descrip_line_max_len,
)
if comment_list and app_obj.comment_store_flag:
video_obj.set_comments(comment_list)
# If downloading from a channel/playlist, remember the video's
# index. (The server supplies an index even for a channel, and
# the user might want to convert a channel to a playlist)
@ -4443,6 +4451,9 @@ class VideoDownloader(object):
app_obj.main_win_obj.descrip_line_max_len,
)
if not video_obj.comment_list and comment_list:
video_obj.set_comments(comment_list)
# If downloading from a channel/playlist, remember the video's
# index. (The server supplies an index even for a channel, and
# the user might want to convert a channel to a playlist)
@ -4559,6 +4570,10 @@ class VideoDownloader(object):
# as the video (but with a different extension)
# Get the thumbnail's extension...
remote_file, remote_ext = os.path.splitext(thumbnail)
# Fix for Odysee videos, whose thumbnail extension is not specified
# in the .info.json fiel
if remote_ext == '':
remote_ext = '.webp'
# ...and thus get the filename used by youtube-dl when storing the
# thumbnail locally
@ -4645,7 +4660,7 @@ class VideoDownloader(object):
)
# For simulated downloads, self.do_download() has not displayed
# anything in the Output Tab/terminal window; so do that now (if
# anything in the Output tab/terminal window; so do that now (if
# required)
if (app_obj.ytdl_output_stdout_flag):
@ -5008,7 +5023,7 @@ class VideoDownloader(object):
dl_stat_dict['filesize'] = stdout_list[3]
# If the most recently-received filename isn't one used by
# Ffmpeg, then this marks the end of a video download
# FFmpeg, then this marks the end of a video download
# (See the comments in self.__init__)
if len(stdout_list) > 4 \
and stdout_list[4] == 'in' \
@ -5301,7 +5316,7 @@ class VideoDownloader(object):
Returns:
True if the child process is alive, otherwise returns False.
True if the child process is alive, otherwise returns False
"""
@ -5569,7 +5584,7 @@ class VideoDownloader(object):
else:
dl_stat_dict['status'] = formats.ERROR_STAGE_ABORT
# Use some empty values in dl_stat_dict so that the Progress Tab
# Use some empty values in dl_stat_dict so that the Progress tab
# doesn't show arbitrary data from the last file downloaded
# Exception: in Classic Mode, don't do that for self.ALREADY, otherwise
# the filename will never be visible
@ -5798,7 +5813,7 @@ class ClipDownloader(object):
self.sleep_time = 0.1
# Flag set to True if this download operation was launched from the
# Classic Mode Tab, False if not (set below)
# Classic Mode tab, False if not (set below)
self.dl_classic_flag = False
# Flag set to True if an attempt to copy an original videos' thumbnail
# fails (in which case, don't try again)
@ -5809,7 +5824,7 @@ class ClipDownloader(object):
# the next clip has been downloaded
self.stop_soon_flag = False
# When self.stop_soon_flag is True, the next call to
# self.extract_stdout_data() for a downlaoded clip sets this flag to
# self.extract_stdout_data() for a downloaded clip sets this flag to
# True, informing self.do_download_clips() that it can stop the child
# process
self.stop_now_flag = False
@ -5857,7 +5872,7 @@ class ClipDownloader(object):
Returns:
The final return code, a value in the range 0-5 (as described
above)
above)
"""
@ -5885,7 +5900,7 @@ class ClipDownloader(object):
if app_obj.video_timestamps_re_extract_flag \
and not orig_video_obj.stamp_list:
app_obj.update_video_from_json(orig_video_obj, True)
app_obj.update_video_from_json(orig_video_obj, 'chapters')
if app_obj.video_timestamps_re_extract_flag \
and not orig_video_obj.stamp_list:
@ -5981,7 +5996,7 @@ class ClipDownloader(object):
self.dl_classic_flag,
)
# ...display it in the Output Tab (if required)...
# ...display it in the Output tab (if required)...
if app_obj.ytdl_output_system_cmd_flag:
app_obj.main_win_obj.output_tab_write_system_cmd(
self.download_worker_obj.worker_id,
@ -5992,7 +6007,7 @@ class ClipDownloader(object):
if app_obj.ytdl_write_system_cmd_flag:
print(' '.join(cmd_list))
# Write an additional message in the Output Tab, in the same style
# Write an additional message in the Output tab, in the same style
# as those produced by youtube-dl/FFmpeg (and therefore not
# translated)
app_obj.main_win_obj.output_tab_write_stdout(
@ -6048,13 +6063,13 @@ class ClipDownloader(object):
stdout = stdout.decode(utils.get_encoding(), 'replace')
# Remove weird carriage returns that insert empty lines
# into the Output Tab
# into the Output tab
stdout = re.sub(r"[\r]+", "", stdout)
# Extract output from stdout
self.extract_stdout_data(stdout)
# Show output in the Output Tab (if required)
# Show output in the Output tab (if required)
if app_obj.ytdl_output_stdout_flag:
app_obj.main_win_obj.output_tab_write_stdout(
@ -6107,7 +6122,7 @@ class ClipDownloader(object):
self.last_data_callback()
return self.STALLED
# Show output in the Output Tab (if required)
# Show output in the Output tab (if required)
if (app_obj.ytdl_output_stderr_flag):
app_obj.main_win_obj.output_tab_write_stderr(
self.download_worker_obj.worker_id,
@ -6170,7 +6185,7 @@ class ClipDownloader(object):
# Delete the original video, if required, and if it's not inside a
# channel/playlist
# (Don't bother trying to delete a 'dummy' media.Vidoe object, for
# (Don't bother trying to delete a 'dummy' media.Video object, for
# download operations launched from the Classic Mode tab)
if app_obj.split_video_auto_delete_flag \
and not isinstance(orig_video_obj.parent_obj, media.Channel) \
@ -6214,7 +6229,7 @@ class ClipDownloader(object):
Returns:
The final return code, a value in the range 0-5 (as described
above)
above)
"""
@ -6326,7 +6341,7 @@ class ClipDownloader(object):
self.dl_classic_flag,
)
# ...display it in the Output Tab (if required)...
# ...display it in the Output tab (if required)...
if app_obj.ytdl_output_system_cmd_flag:
app_obj.main_win_obj.output_tab_write_system_cmd(
self.download_worker_obj.worker_id,
@ -6337,7 +6352,7 @@ class ClipDownloader(object):
if app_obj.ytdl_write_system_cmd_flag:
print(' '.join(cmd_list))
# Write an additional message in the Output Tab, in the same style
# Write an additional message in the Output tab, in the same style
# as those produced by youtube-dl/FFmpeg (and therefore not
# translated)
app_obj.main_win_obj.output_tab_write_stdout(
@ -6396,13 +6411,13 @@ class ClipDownloader(object):
stdout = stdout.decode(utils.get_encoding(), 'replace')
# Remove weird carriage returns that insert empty lines
# into the Output Tab
# into the Output tab
stdout = re.sub(r"[\r]+", "", stdout)
# Extract output from stdout
self.extract_stdout_data(stdout)
# Show output in the Output Tab (if required)
# Show output in the Output tab (if required)
if app_obj.ytdl_output_stdout_flag:
app_obj.main_win_obj.output_tab_write_stdout(
@ -6455,7 +6470,7 @@ class ClipDownloader(object):
self.last_data_callback()
return self.STALLED
# Show output in the Output Tab (if required)
# Show output in the Output tab (if required)
if (app_obj.ytdl_output_stderr_flag):
app_obj.main_win_obj.output_tab_write_stderr(
self.download_worker_obj.worker_id,
@ -6574,7 +6589,7 @@ class ClipDownloader(object):
output_path,
]
# ...display it in the Output Tab (if required)...
# ...display it in the Output tab (if required)...
if app_obj.ytdl_output_system_cmd_flag:
app_obj.main_win_obj.output_tab_write_system_cmd(
self.download_worker_obj.worker_id,
@ -6634,15 +6649,10 @@ class ClipDownloader(object):
),
)
try:
if os.path.isfile(moved_path):
os.remove(moved_path)
shutil.move(output_path, moved_path)
except:
if os.path.isfile(moved_path):
app_obj.remove_file(moved_path)
if not app_obj.move_file_or_directory(output_path, moved_path):
app_obj.main_win_obj.output_tab_write_stderr(
self.download_worker_obj.worker_id,
_(
@ -6666,7 +6676,7 @@ class ClipDownloader(object):
self.confirm_video_remove_slices(orig_video_obj, moved_path)
# Delete the temporary directory
shutil.rmtree(temp_dir)
app_obj.remove_directory(temp_dir)
# Pass a dictionary of values to downloads.DownloadWorker, confirming
# the result of the job. The values are passed on to the main
@ -6815,7 +6825,7 @@ class ClipDownloader(object):
pass
# Special case: if the download operation was launched from the
# Classic Mode Tab, then we only need to update the dummy
# Classic Mode tab, then we only need to update the dummy
# media.Video object, and to move/remove description/metadata/
# thumbnail files, as appropriate
elif self.dl_classic_flag:
@ -6945,9 +6955,9 @@ class ClipDownloader(object):
# ...then create it
try:
if os.path.isdir(temp_dir):
shutil.rmtree(temp_dir)
app_obj.remove_directory(temp_dir)
os.makedirs(temp_dir)
app_obj.make_directory(temp_dir)
return temp_dir
@ -7032,7 +7042,7 @@ class ClipDownloader(object):
Returns:
True if the child process is alive, otherwise returns False.
True if the child process is alive, otherwise returns False
"""
@ -7190,7 +7200,7 @@ class ClipDownloader(object):
if not os.path.isfile(moved_path) \
and not os.path.isfile(final_path):
shutil.move(descrip_path, moved_path)
app_obj.move_file_or_directory(descrip_path, moved_path)
# Further move the file into its sub-directory, if
# required, first creating that sub-directory if it
@ -7231,7 +7241,7 @@ class ClipDownloader(object):
if not os.path.isfile(moved_path) \
and not os.path.isfile(final_path):
shutil.move(json_path, moved_path)
app_obj.move_file_or_directory(json_path, moved_path)
if options_obj.options_dict['move_info']:
utils.move_metadata_to_subdir(
@ -7271,7 +7281,7 @@ class ClipDownloader(object):
#
# if not os.path.isfile(moved_path) \
# and not os.path.isfile(final_path):
# shutil.move(xml_path, moved_path)
# app_obj.move_file_or_directory(xml_path, moved_path)
#
# if options_obj.options_dict['move_annotations']:
# utils.move_metadata_to_subdir(
@ -7301,7 +7311,7 @@ class ClipDownloader(object):
)
if not os.path.isfile(moved_path):
shutil.move(thumb_path, moved_path)
app_obj.move_file_or_directory(thumb_path, moved_path)
# Convert .webp thumbnails to .jpg, if required
convert_path = utils.find_thumbnail_webp(
@ -7561,7 +7571,7 @@ class StreamDownloader(object):
Returns:
The final return code, a value in the range 0-5 (as described
above)
above)
"""
@ -7572,7 +7582,7 @@ class StreamDownloader(object):
app_obj = self.download_manager_obj.app_obj
video_obj = self.download_item_obj.media_data_obj
# This object creates more messages for the Output Tab and/or terminal,
# This object creates more messages for the Output tab and/or terminal,
# than downloads.VideoDownloader would do, as the output generated by
# Youtube Stream Capture is not easy for the user to interpret
self.show_msg(_('Tartube is starting the stream capture...'))
@ -7589,7 +7599,7 @@ class StreamDownloader(object):
try:
if not os.path.isdir(self.temp_dir_path):
os.makedirs(self.temp_dir_path)
app_obj.make_directory(self.temp_dir_path)
except:
self.download_item_obj.media_data_obj.set_error(
@ -7668,7 +7678,7 @@ class StreamDownloader(object):
+ [video_obj.source] + ['--output-directory'] \
+ [self.temp_dir_path]
# ...and display it in the Output Tab/terminal, if required
# ...and display it in the Output tab/terminal, if required
self.show_cmd(' '.join(cmd_list))
# Create a new child process using that system command...
@ -7753,7 +7763,7 @@ class StreamDownloader(object):
# Generate the system command...
cmd_list = ['python3'] + [self.ytsc_merge_path] + [video_obj.source] \
+ ['--output-directory'] + [self.temp_dir_path]
# ...and display it in the Output Tab/terminal, if required
# ...and display it in the Output tab/terminal, if required
self.show_cmd(' '.join(cmd_list))
# Create a new child process using that system command...
@ -7893,7 +7903,7 @@ class StreamDownloader(object):
write_flag = False
# Intercept the segment number, and update the IV (but
# don't display that line in the Output Tab)
# don't display that line in the Output tab)
match = re.search('^Segment number\: (\d+)', stdout)
if match:
@ -7949,7 +7959,7 @@ class StreamDownloader(object):
self.download_worker_obj.data_callback(dl_stat_dict)
# Show output in the Output Tab (if required)
# Show output in the Output tab (if required)
if app_obj.ytsc_write_verbose_flag \
or (app_obj.ytdl_output_stdout_flag and write_flag):
app_obj.main_win_obj.output_tab_write_stdout(
@ -8091,7 +8101,7 @@ class StreamDownloader(object):
self.download_worker_obj.data_callback(dl_stat_dict)
# Show output in the Output Tab (if required)
# Show output in the Output tab (if required)
if app_obj.ytsc_write_verbose_flag \
or (app_obj.ytdl_output_stdout_flag and write_flag):
app_obj.main_win_obj.output_tab_write_stdout(
@ -8192,7 +8202,7 @@ class StreamDownloader(object):
Returns:
True if the child process is alive, otherwise returns False.
True if the child process is alive, otherwise returns False
"""
@ -8233,7 +8243,7 @@ class StreamDownloader(object):
elif self.return_code == self.STOPPED:
dl_stat_dict['status'] = formats.ERROR_STAGE_STOPPED
# Use some empty values in dl_stat_dict so that the Progress Tab
# Use some empty values in dl_stat_dict so that the Progress tab
# doesn't show arbitrary data from the last file downloaded
dl_stat_dict['playlist_index'] = ''
dl_stat_dict['playlist_size'] = ''
@ -8270,7 +8280,7 @@ class StreamDownloader(object):
"""Called by self.do_capture() and .do_merge().
Shows a system command in the Output Tab and/or terminal window, if
Shows a system command in the Output tab and/or terminal window, if
required.
Args:
@ -8285,7 +8295,7 @@ class StreamDownloader(object):
# Import the main app (for convenience)
app_obj = self.download_manager_obj.app_obj
# Display the command in the Output Tab, if allowed
# Display the command in the Output tab, if allowed
if app_obj.ytdl_output_system_cmd_flag:
app_obj.main_win_obj.output_tab_write_system_cmd(
@ -8305,7 +8315,7 @@ class StreamDownloader(object):
"""Called by self.do_capture() and .do_merge().
Shows a message in the Output Tab and/or terminal window, if required.
Shows a message in the Output tab and/or terminal window, if required.
Args:
@ -8319,7 +8329,7 @@ class StreamDownloader(object):
# Import the main app (for convenience)
app_obj = self.download_manager_obj.app_obj
# Display the message in the Output Tab, if allowed
# Display the message in the Output tab, if allowed
if app_obj.ytdl_output_stdout_flag:
app_obj.main_win_obj.output_tab_write_stdout(
@ -9325,7 +9335,7 @@ class MiniJSONFetcher(object):
Returns:
True if the child process is alive, otherwise returns False.
True if the child process is alive, otherwise returns False
"""
@ -9603,7 +9613,7 @@ class PipeReader(threading.Thread):
Warnings:
All the actions are based on 'str' types. The calling function must
convert the queued items back to 'unicode', if necessary.
convert the queued items back to 'unicode', if necessary
"""

View File

@ -159,7 +159,7 @@ class FFmpegManager(object):
else:
# Conversion succeeded
os.remove(escaped_thumbnail_filename)
self.app_obj.remove_file(escaped_thumbnail_filename)
thumbnail_jpg_filename = self.replace_extension(
thumbnail_filename,
'jpg',

View File

@ -250,7 +250,9 @@ video_option_setup_list = [
'96', 'hls [1080p] <96>', False,
'139', 'm4a 48k (DASH Audio) <139>', True,
'140', 'm4a 128k (DASH Audio) <140>', True,
'256', 'm4a 192k (DASH Audio) <256>', True,
'141', 'm4a 256k (DASH Audio) <141>', True,
'258', 'm4a 384k (DASH Audio) <258>', True,
'18', 'mp4 [360p] <18>', False,
'22', 'mp4 [720p] <22>', False,
'37', 'mp4 [1080p] <37>', False,
@ -279,6 +281,7 @@ video_option_setup_list = [
'400', 'mp4 [1440p] <400>', False,
'401', 'mp4 [2160p] <401>', False,
'402', 'mp4 [2880p] <402>', False,
'571', 'mp4 [8k] <571', False,
'43', 'webm [360p] <43>', False,
'44', 'webm [480p] <44>', False,
'45', 'webm [720p] <45>', False,
@ -757,6 +760,7 @@ SMALL_ICON_DICT = {
'arrow_up_small': 'arrow_up.png',
'arrow_down_small': 'arrow_down.png',
'check_small': 'check.png',
'comment_small': 'comment.png',
'debut_now_small': 'debut_now.png',
'debut_wait_small': 'debut_wait.png',
'delete_small': 'delete.png',
@ -764,20 +768,23 @@ SMALL_ICON_DICT = {
'download_small': 'download.png',
'error_small': 'error.png',
'external_small': 'external.png',
'favourite_small': 'favourite.png',
'folder_black_small': 'folder_black.png',
'folder_blue_small': 'folder_blue.png',
'folder_green_small': 'folder_green.png',
'folder_red_small': 'folder_red.png',
'likes_small': 'likes.png',
'have_file_small': 'have_file.png',
'live_now_small': 'live_now.png',
'live_wait_small': 'live_wait.png',
'no_file_small': 'no_file.png',
'slice_small': 'slice.png',
'split_file_small': 'split_file.png',
'sponsorblock_small': 'sponsorblock.png',
'stamp_small': 'stamp.png',
'system_error_small': 'system_error.png',
'system_warning_small': 'system_warning.png',
'timestamp_small': 'timestamp.png',
'unavailable_small': 'unavailable.png',
'uploader_small': 'uploader.png',
'warning_small': 'warning.png',
}

View File

@ -188,7 +188,7 @@ class InfoManager(threading.Thread):
return self.run_check_version()
# Show information about the info operation in the Output Tab
# Show information about the info operation in the Output tab
if self.info_type == 'test_ytdl':
msg = _(
@ -270,7 +270,7 @@ class InfoManager(threading.Thread):
# Create the new child process
self.create_child_process(cmd_list)
# Show the system command in the Output Tab
# Show the system command in the Output tab
space = ' '
self.app_obj.main_win_obj.output_tab_write_system_cmd(
1,
@ -303,7 +303,7 @@ class InfoManager(threading.Thread):
self.output_list.append(stdout)
self.stdout_list.append(stdout)
# Show command line output in the Output Tab
# Show command line output in the Output tab
self.app_obj.main_win_obj.output_tab_write_stdout(
1,
stdout,
@ -336,7 +336,7 @@ class InfoManager(threading.Thread):
else:
self.stderr_list.append(stderr)
# Show command line output in the Output Tab
# Show command line output in the Output tab
self.app_obj.main_win_obj.output_tab_write_stderr(
1,
stderr,
@ -368,7 +368,7 @@ class InfoManager(threading.Thread):
if not self.stderr_list:
self.success_flag = True
# Show a confirmation in the the Output Tab
# Show a confirmation in the the Output tab
self.app_obj.main_win_obj.output_tab_write_stdout(
1,
_('Info operation finished'),
@ -396,7 +396,7 @@ class InfoManager(threading.Thread):
can display them.
"""
# Show information about the info operation in the Output Tab
# Show information about the info operation in the Output tab
self.app_obj.main_win_obj.output_tab_write_stdout(
1,
_('Starting info operation, checking for new releases of Tartube'),
@ -487,7 +487,7 @@ class InfoManager(threading.Thread):
# mainapp.TartubeApp.info_manager_finished()
self.success_flag = True
# Show a confirmation in the the Output Tab
# Show a confirmation in the the Output tab
self.app_obj.main_win_obj.output_tab_write_stdout(
1,
_('Info operation finished'),

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -228,7 +228,7 @@ class GenericContainer(GenericMedia):
Args:
data_type (str): 'receive' to compile vidoe frequencies by
data_type (str): 'receive' to compile video frequencies by
receive (download) time, 'download' to compile by download time
period (int): A time period, in seconds (e.g. 86400 for a day)
@ -724,7 +724,7 @@ class GenericContainer(GenericMedia):
Returns:
True or False.
True or False
"""
@ -1666,8 +1666,8 @@ class Video(GenericMedia):
# IV list - other
# ---------------
# Unique media data object ID (an integer)
# When a download operation is launched from the Classic Mode Tab,
# the code creates a series of dummy media.Video objects that aren't
# When a download operation is launched from the Classic Mode tab, the
# code creates a series of dummy media.Video objects that aren't
# added to the media data registry. Those dummy objects have negative
# dbids
self.dbid = dbid
@ -1829,6 +1829,27 @@ class Video(GenericMedia):
# SponsorBlock. This valus is not required by Tartube code,
# and its default value is 0
self.slice_list = []
# List containing video comments, extracted from the video's metadata.
# Only popuplated when downloading the video with yt-dlp
# List of dictionaries, sorted by timestamp (most recent first). Each
# dictionary contains a reduced set of the keys extracted from yt-dl
# data, including these compulsory items:
# ['id']: (int) Simple seequential integer ID, the first omment
# added to the list is 1
# ['text']: (str) Text of the comment itself
# ['parent']: (int): ID of the parent comment, or None if no
# parent
# These items are optional:
# ['timestamp']: (int) Epoch timestamp of the comment. As of
# v2.3.318, all comments in a YouTube video share the same
# timestamp
# ['time']: (str) String describing the comment age, e.g. '3 days
# ago'
# ['author']: (str) Name of comment author
# ['likes']: (int) Number of likes
# ['fav_flag']: (bool) True if comment favourited, False if not
# ['ul_flag']: (bool) True if commenter is uploader, False if not
self.comment_list = []
# List of error/warning messages generated the last time the video was
# checked or downloaded. Both set to empty lists if the video has
@ -1841,7 +1862,7 @@ class Video(GenericMedia):
self.warning_list = []
# IVs used only when the download operation is launched from the
# Classic Mode Tab
# Classic Mode tab
# Flag set to True if this is a dummy media.Video object
self.dummy_flag = False
# The destination directory for the download
@ -2012,7 +2033,7 @@ class Video(GenericMedia):
"""
descrip_path = self.check_actual_path_by_ext(app_obj, '.description')
if (descrip_path):
if descrip_path:
text = app_obj.file_manager_obj.load_text(descrip_path)
if text is not None:
@ -2288,6 +2309,87 @@ class Video(GenericMedia):
self.stamp_list = []
def set_comments(self, comment_list):
"""Can be called by anything.
Sets the video's comments list, after sorting it.
"""
# 'comment_list' contains a sequence of dictionaries. Some of the keys
# in the dictionaries are not required, and must be removed
# The key is the original field provided by yt-dlp, the corresponding
# value is the field used by media.Video
check_dict = {
'id': 'id',
'text': 'text',
'timestamp': 'timestamp',
'time_text': 'time',
'like_count': 'likes',
'is_favorited': 'fav_flag',
'author': 'author',
'author_is_uploader': 'ul_flag',
'parent': 'parent',
}
# Use simple sequential integers for the 'id' and 'parent' fields
id_count = 1
parent_dict = {}
# Process each comment
new_list = []
for mini_dict in comment_list:
new_dict = {}
for key in mini_dict.keys():
if key in check_dict and mini_dict[key] is not None:
if key == 'id':
this_id = id_count
parent_dict[mini_dict[key]] = this_id
new_dict['id'] = this_id
id_count += 1
elif key == 'parent':
if not mini_dict[key] in parent_dict:
new_dict['parent'] = None
else:
new_dict['parent'] = parent_dict[mini_dict[key]]
else:
new_dict[check_dict[key]] = mini_dict[key]
# This key is also compulosry; add a null parent, if not found
if not 'parent' in new_dict:
new_dict['parent'] = None
# These keys are compulsory, ignore the comment if they're not
# found
if 'id' in new_dict and 'text' in new_dict:
new_list.append(new_dict)
# Sort comments by timestamp
# v2.3.317 disabled, since all timestamps are the same for each video
# at the moment
# new_list = list(sorted(new_list, key=lambda x:x['time']))
# Update the IV
self.comment_list = new_list
def reset_comments(self):
"""Can be called by anything.
Empties the video's comment list.
"""
self.comment_list = []
# Set accessors
@ -3070,11 +3172,11 @@ class Channel(GenericRemoteContainer):
dbid (int): A unique ID for this media data object
name (str) - The channel name
name (str): The channel name
parent_obj (media.Folder) - The parent media data object, if any
parent_obj (media.Folder): The parent media data object, if any
options_obj (options.OptionsManager) - The object specifying download
options_obj (options.OptionsManager): The object specifying download
options for this channel, if any
"""
@ -3259,11 +3361,11 @@ class Playlist(GenericRemoteContainer):
dbid (int): A unique ID for this media data object
name (str) - The playlist name
name (str): The playlist name
parent_obj (media.Folder) - The parent media data object, if any
parent_obj (media.Folder): The parent media data object, if any
options_obj (options.OptionsManager) - The object specifying download
options_obj (options.OptionsManager): The object specifying download
options for this channel, if any
"""
@ -3450,25 +3552,25 @@ class Folder(GenericContainer):
dbid (int): A unique ID for this media data object
name (str) - The folder name
name (str): The folder name
parent_obj (media.Folder) - The parent media data object, if any
parent_obj (media.Folder): The parent media data object, if any
options_obj (options.OptionsManager) - The object specifying download
options_obj (options.OptionsManager): The object specifying download
options for this channel, if any
fixed_flag (bool) - If True, this folder can't be deleted by the user
fixed_flag (bool): If True, this folder can't be deleted by the user
priv_flag (bool) - If True, the user can't add anything to this folder,
priv_flag (bool): If True, the user can't add anything to this folder,
because Tartube uses it for special purposes
restrict_mode (str) - 'full' if this folder can contain videos, but not
restrict_mode (str): 'full' if this folder can contain videos, but not
channels/playlists/folders, 'partial' if this folder can contain
videos and folders, but not channels and playlists, 'open' if this
folder can contain any combination of videos, channels, playlists
and folders
temp_flag (bool) - If True, the folder's contents should be deleted
temp_flag (bool): If True, the folder's contents should be deleted
when Tartube shuts down (but the folder itself remains)
"""
@ -3787,7 +3889,7 @@ class Scheduled(object):
custom downloads; the value is checked before being used, and
converted to 'custom_sim' where necessary)
start_mode (str) 'none' to disable this schedule, 'start' to perform
start_mode (str): 'none' to disable this schedule, 'start' to perform
the operation whenever Tartube starts, or 'scheduled' to perform
the operation at regular intervals

View File

@ -354,7 +354,7 @@ class OptionsManager(object):
[Download Options]
concurrent_fragments (int): Number of fragments of a dash/hlsnative
concurrent_fragments (int): Number of fragments of a DASH/hlsnative
video that should be download concurrently (default is 1)
throttled_rate (int): Minimum download rate in bytes per second below
@ -380,10 +380,6 @@ class OptionsManager(object):
no_clean_info_json (bool): If True, writes all fields to the infojson
(default is to remove some private fields)
write_comments (bool): If True, retrieves video comments to be placed
in the .info.json. The comments are fetched even without this
option if the extraction is known to be quick
[Internet Shortcut Options]
write_link (bool): If True, writes an internet shortcut file, depending
@ -808,7 +804,6 @@ class OptionsManager(object):
'force_overwrites': False,
'write_playlist_metafiles': False,
'no_clean_info_json': False,
'write_comments': False,
# (Internet Shortcut Options)
'write_link': False,
'write_url_link': False,
@ -870,7 +865,7 @@ class OptionsManager(object):
"""Called by mainapp.TartubeApp.apply_classic_download_options().
When the user applies download options in the Classic Mode Tab, a few
When the user applies download options in the Classic Mode tab, a few
options should have different default values; this function sets them.
"""
@ -1177,8 +1172,6 @@ class OptionsParser(object):
),
# --no-clean-infojson
OptionHolder('no_clean_info_json', '--no-clean-infojson', False),
# --write-comments
OptionHolder('write_comments', '--write-comments', False),
# (Internet Shortcut Options)
# --write-link
OptionHolder('write_link', '--write-link', False),
@ -1611,7 +1604,7 @@ class OptionsParser(object):
or operation_type == 'classic_custom':
# Special case: if a download operation was launched from the
# Classic Mode Tab, the directory is specified in that tab
# Classic Mode tab, the directory is specified in that tab
dir_path = media_data_obj.dummy_dir
elif not isinstance(media_data_obj, media.Video) \
@ -1696,7 +1689,7 @@ class OptionsParser(object):
if isinstance(media_data_obj, media.Video):
# Special case: if a download operation was launched from the
# Classic Mode Tab, the video format may be specified by that tab
# Classic Mode tab, the video format may be specified by that tab
if (
operation_type == 'classic_sim' \
or operation_type == 'classic_real' \
@ -1718,7 +1711,7 @@ class OptionsParser(object):
# Download the video in the specified format, if available
# Ignore all video/audio formats except the one specified
# by the user in the Classic Mode Tab
# by the user in the Classic Mode tab
copy_dict['video_format'] = format_str
copy_dict['all_formats'] = False
copy_dict['video_format_list'] = []
@ -1736,7 +1729,7 @@ class OptionsParser(object):
# Converting video formats requires post-processing
# Ignore all video/audio formats except the one specified
# by the user in the Classic Mode Tab
# by the user in the Classic Mode tab
copy_dict['video_format'] = '0'
copy_dict['all_formats'] = False
copy_dict['video_format_list'] = []

View File

@ -146,7 +146,7 @@ class ProcessManager(threading.Thread):
complete.
"""
# Show information about the process operation in the Output Tab
# Show information about the process operation in the Output tab
self.app_obj.main_win_obj.output_tab_write_stdout(
1,
_('Starting process operation'),
@ -160,7 +160,7 @@ class ProcessManager(threading.Thread):
video_obj = self.video_list.pop(0)
self.job_count += 1
# Update our progress in the Output Tab
# Update our progress in the Output tab
self.app_obj.main_win_obj.output_tab_write_stdout(
1,
_('Video') + ' ' + str(self.job_count) + '/' \
@ -209,7 +209,7 @@ class ProcessManager(threading.Thread):
# Operation complete. Set the stop time
self.stop_time = int(time.time())
# Show a confirmation in the Output Tab
# Show a confirmation in the Output tab
self.app_obj.main_win_obj.output_tab_write_stdout(
1,
_('Process operation finished'),
@ -302,9 +302,9 @@ class ProcessManager(threading.Thread):
# ...then create it
try:
if os.path.isdir(temp_dir):
shutil.rmtree(temp_dir)
self.app_obj.remove_directory(temp_dir)
os.makedirs(temp_dir)
self.app_obj.make_directory(temp_dir)
return temp_dir
@ -416,7 +416,7 @@ class ProcessManager(threading.Thread):
self.job_total,
)
# Update the Output Tab again
# Update the Output tab again
self.app_obj.main_win_obj.output_tab_write_system_cmd(
1,
' '.join(cmd_list),
@ -460,12 +460,7 @@ class ProcessManager(threading.Thread):
and os.path.isfile(output_path) \
and source_path != output_path:
try:
os.remove(source_path)
except:
if not self.app_obj.remove_file(source_path):
self.fail_count += 1
self.app_obj.main_win_obj.output_tab_write_stderr(
@ -509,21 +504,17 @@ class ProcessManager(threading.Thread):
),
)
# (Don't call utils.rename_file(), as we need our own
# try/except)
try:
# (On MSWin, can't do os.rename if the destination file
# already exists)
if os.path.isfile(new_thumb_path):
os.remove(new_thumb_path)
# (os.rename sometimes fails on external hard drives;
# this is safer)
shutil.move(thumb_path, new_thumb_path)
except:
# (On MSWin, can't do os.rename if the destination file
# already exists)
if os.path.isfile(new_thumb_path):
self.app_obj.remove_file(new_thumb_path)
# (os.rename sometimes fails on external hard drives; this
# is safer)
if not self.app_obj.move_file_or_directory(
thumb_path,
new_thumb_path,
):
self.fail_count += 1
self.app_obj.main_win_obj.output_tab_write_stderr(
@ -647,7 +638,7 @@ class ProcessManager(threading.Thread):
start_time = mini_list[0]
stop_time = mini_list[1]
# Update the Output Tab
# Update the Output tab
if not stop_time:
self.app_obj.main_win_obj.output_tab_write_stdout(
@ -675,7 +666,7 @@ class ProcessManager(threading.Thread):
# Don't continue creating more clips after an error
self.fatal_error_flag = True
# (Delete the temporary directory after failure)
shutil.rmtree(temp_dir)
self.app_obj.remove_directory(temp_dir)
return None
# If there is more than one clip, they must be concatenated to produce
@ -727,7 +718,7 @@ class ProcessManager(threading.Thread):
output_path,
]
# Update the Output Tab again
# Update the Output tab again
self.app_obj.main_win_obj.output_tab_write_system_cmd(
1,
' '.join(cmd_list),
@ -751,20 +742,18 @@ class ProcessManager(threading.Thread):
# (Delete the temporary directory after failure)
self.fail_count += 1
shutil.rmtree(temp_dir)
self.app_obj.remove_directory(temp_dir)
return None
# Move the single video file back into the parent directory,
# replacing any file of the same name that's already there
try:
if os.path.isfile(orig_video_path):
os.remove(orig_video_path)
shutil.move(output_path, orig_video_path)
except:
# Move the single video file back into the parent directory, replacing
# any file of the same name that's already there
if os.path.isfile(orig_video_path):
self.app_obj.remove_file(orig_video_path)
if not self.app_obj.move_file_or_directory(
output_path,
orig_video_path,
):
self.app_obj.main_win_obj.output_tab_write_stderr(
1,
_(
@ -775,11 +764,11 @@ class ProcessManager(threading.Thread):
# (Delete the temporary directory after failure)
self.fail_count += 1
shutil.rmtree(temp_dir)
self.app_obj.remove_directory(temp_dir)
return None
# Delete the temporary directory
shutil.rmtree(temp_dir)
self.app_obj.remove_directory(temp_dir)
# Procedure successful
return parent_dir
@ -808,7 +797,7 @@ class ProcessManager(threading.Thread):
if self.app_obj.video_timestamps_re_extract_flag \
and not orig_video_obj.stamp_list:
self.app_obj.update_video_from_json(orig_video_obj, True)
self.app_obj.update_video_from_json(orig_video_obj, 'chapters')
if self.app_obj.video_timestamps_re_extract_flag \
and not orig_video_obj.stamp_list:
@ -885,7 +874,7 @@ class ProcessManager(threading.Thread):
self.clip_title_dict[clip_title] = None
# Update the Output Tab
# Update the Output tab
if not stop_stamp:
self.app_obj.main_win_obj.output_tab_write_stdout(

View File

@ -132,7 +132,7 @@ class RefreshManager(threading.Thread):
complete.
"""
# Show information about the refresh operation in the Output Tab
# Show information about the refresh operation in the Output tab
if not self.init_obj:
self.app_obj.main_win_obj.output_tab_write_stdout(
1,
@ -186,7 +186,7 @@ class RefreshManager(threading.Thread):
# Operation complete. Set the stop time
self.stop_time = int(time.time())
# Show a confirmation in the Output Tab
# Show a confirmation in the Output tab
self.app_obj.main_win_obj.output_tab_write_stdout(
1,
_('Refresh operation finished'),
@ -254,7 +254,7 @@ class RefreshManager(threading.Thread):
local_match_count = 0
local_new_count = 0
# Update our progress in the Output Tab
# Update our progress in the Output tab
if isinstance(media_data_obj, media.Channel):
string = _('Channel:') + ' '
elif isinstance(media_data_obj, media.Playlist):
@ -404,7 +404,7 @@ class RefreshManager(threading.Thread):
# match it
del check_dict[filename]
# Update our progress in the Output Tab (if required)
# Update our progress in the Output tab (if required)
if self.app_obj.refresh_output_videos_flag:
self.app_obj.main_win_obj.output_tab_write_stdout(
1,
@ -457,7 +457,7 @@ class RefreshManager(threading.Thread):
# If the video's JSON file exists downloaded, we can extract
# video statistics from it
self.app_obj.update_video_from_json(video_obj)
self.app_obj.update_video_from_json(video_obj, 'chapters')
# For any of those statistics that haven't been set (because
# the JSON file was missing or didn't contain the right
@ -524,7 +524,7 @@ class RefreshManager(threading.Thread):
# (No new media.Video objects are created)
local_missing_count = 0
# Update our progress in the Output Tab
# Update our progress in the Output tab
if isinstance(media_data_obj, media.Channel):
string = _('Channel:') + ' '
elif isinstance(media_data_obj, media.Playlist):
@ -561,7 +561,7 @@ class RefreshManager(threading.Thread):
# Video doesn't exist, so mark it as not downloaded
self.app_obj.mark_video_downloaded(child_obj, False)
# Update our progress in the Output Tab (if required)
# Update our progress in the Output tab (if required)
self.app_obj.main_win_obj.output_tab_write_stdout(
1,
' ' + _('Missing:') + ' ' + child_obj.name,
@ -578,7 +578,7 @@ class RefreshManager(threading.Thread):
# as new)
self.app_obj.mark_video_downloaded(child_obj, True, True)
# Update our progress in the Output Tab (if required)
# Update our progress in the Output tab (if required)
if self.app_obj.refresh_output_videos_flag:
self.app_obj.main_win_obj.output_tab_write_stdout(
1,

View File

@ -42,8 +42,8 @@ import mainapp
# 'Global' variables
__packagename__ = 'tartube'
__version__ = '2.3.306'
__date__ = '3 Aug 2021'
__version__ = '2.3.321'
__date__ = '5 Aug 2021'
__copyright__ = 'Copyright \xa9 2019-2021 A S Lewis'
__license__ = """
Copyright \xa9 2019-2021 A S Lewis.

View File

@ -223,7 +223,7 @@ class TidyManager(threading.Thread):
complete.
"""
# Show information about the tidy operation in the Output Tab
# Show information about the tidy operation in the Output tab
if not self.init_obj:
self.app_obj.main_win_obj.output_tab_write_stdout(
1,
@ -406,7 +406,7 @@ class TidyManager(threading.Thread):
# Operation complete. Set the stop time
self.stop_time = int(time.time())
# Show a confirmation in the Output Tab
# Show a confirmation in the Output tab
self.app_obj.main_win_obj.output_tab_write_stdout(
1,
_('Tidy operation finished'),
@ -604,6 +604,9 @@ class TidyManager(threading.Thread):
"""
# Import the main window (for convenience)
main_win_obj = self.app_obj.main_win_obj
for video_obj in media_data_obj.compile_all_videos( [] ):
if video_obj.file_name is not None \
@ -637,21 +640,29 @@ class TidyManager(threading.Thread):
and os.path.isfile(video_path):
# Delete the corrupted file
os.remove(video_path)
if self.app_obj.remove_file(video_path):
self.video_corrupt_deleted_count += 1
self.video_corrupt_deleted_count += 1
self.app_obj.main_win_obj.output_tab_write_stdout(
1,
' ' + _(
main_win_obj.output_tab_write_stdout(
1,
' ' + _(
'Deleted (possibly) corrupted video file:',
) + ' \'' + video_obj.name + '\'',
)
) + ' \'' + video_obj.name + '\'',
)
self.app_obj.mark_video_downloaded(
video_obj,
False,
)
self.app_obj.mark_video_downloaded(
video_obj,
False,
)
else:
main_win_obj.output_tab_write_stderr(
1,
' ' + _(
'Failed to delete (possibly)' \
+ ' corrupted video file:',
) + ' \'' + video_obj.name + '\'',
)
else:
@ -764,12 +775,11 @@ class TidyManager(threading.Thread):
and os.path.isfile(video_path):
# Delete the downloaded video file
os.remove(video_path)
if self.app_obj.remove_file(video_path):
# Mark the video as not downloaded
self.app_obj.mark_video_downloaded(video_obj, False)
self.video_deleted_count += 1
# Mark the video as not downloaded
self.app_obj.mark_video_downloaded(video_obj, False)
self.video_deleted_count += 1
if self.del_others_flag:
@ -786,9 +796,8 @@ class TidyManager(threading.Thread):
ext,
)
if os.path.isfile(other_path):
os.remove(other_path)
if os.path.isfile(other_path) \
and self.app_obj.remove_file(other_path):
self.other_deleted_count += 1
# For an encore, delete all post-processing artefacts in the form
@ -813,9 +822,8 @@ class TidyManager(threading.Thread):
os.path.join(search_path, check_path),
)
if os.path.isfile(full_path):
os.remove(full_path)
if os.path.isfile(full_path) \
and self.app_obj.remove_file(full_path):
self.other_deleted_count += 1
@ -840,10 +848,9 @@ class TidyManager(threading.Thread):
),
)
if os.path.isfile(archive_path):
# Delete the archive file
os.remove(archive_path)
# Delete the archive file
if os.path.isfile(archive_path) \
and self.app_obj.remove_file(archive_path):
self.archive_deleted_count += 1
@ -899,16 +906,15 @@ class TidyManager(threading.Thread):
if os.path.isfile(main_path) \
and not os.path.isfile(subdir_path):
try:
if not os.path.isdir(subdir):
os.makedirs(subdir)
if not os.path.isdir(subdir):
self.app_obj.make_directory(subdir)
shutil.move(main_path, subdir_path)
if self.app_obj.move_file_or_directory(
main_path,
subdir_path,
):
self.thumb_moved_count += 1
except:
pass
def delete_thumb(self, media_data_obj):
@ -944,11 +950,10 @@ class TidyManager(threading.Thread):
thumb_path,
)
# Delete the thumbnail file
if thumb_path is not None \
and os.path.isfile(thumb_path):
# Delete the thumbnail file
os.remove(thumb_path)
and os.path.isfile(thumb_path) \
and self.app_obj.remove_file(thumb_path):
self.thumb_deleted_count += 1
@ -1048,18 +1053,17 @@ class TidyManager(threading.Thread):
if os.path.isfile(main_path) \
and not os.path.isfile(subdir_path):
try:
if not os.path.isdir(subdir):
os.makedirs(subdir)
if not os.path.isdir(subdir):
self.app_obj.make_directory(subdir)
# (os.rename sometimes fails on external hard
# drives; this is safer)
shutil.move(main_path, subdir_path)
# (os.rename sometimes fails on external hard drives;
# this is safer)
if self.app_obj.move_file_or_directory(
main_path,
subdir_path,
):
self.data_moved_count += 1
except:
pass
def delete_descrip(self, media_data_obj):
@ -1095,11 +1099,10 @@ class TidyManager(threading.Thread):
main_path,
)
# Delete the description file
if main_path is not None \
and os.path.isfile(main_path):
# Delete the description file
os.remove(main_path)
and os.path.isfile(main_path) \
and self.app_obj.remove_file(main_path):
self.descrip_deleted_count += 1
# (Repeat for a file that might be in the sub-directory
@ -1116,9 +1119,8 @@ class TidyManager(threading.Thread):
)
if subdir_path is not None \
and os.path.isfile(subdir_path):
os.remove(subdir_path)
and os.path.isfile(subdir_path) \
and self.app_obj.remove_file(subdir_path):
self.descrip_deleted_count += 1
@ -1156,11 +1158,10 @@ class TidyManager(threading.Thread):
main_path,
)
# Delete the metadata file
if main_path is not None \
and os.path.isfile(main_path):
# Delete the metadata file
os.remove(main_path)
and os.path.isfile(main_path) \
and self.app_obj.remove_file(main_path):
self.json_deleted_count += 1
# (Repeat for a file that might be in the sub-directory
@ -1177,9 +1178,8 @@ class TidyManager(threading.Thread):
)
if subdir_path is not None \
and os.path.isfile(subdir_path):
os.remove(subdir_path)
and os.path.isfile(subdir_path) \
and self.app_obj.remove_file(subdir_path):
self.json_deleted_count += 1
@ -1217,11 +1217,10 @@ class TidyManager(threading.Thread):
main_path,
)
# Delete the annotation file
if main_path is not None \
and os.path.isfile(main_path):
# Delete the annotation file
os.remove(main_path)
and os.path.isfile(main_path) \
and self.app_obj.remove_file(main_path):
self.xml_deleted_count += 1
# (Repeat for a file that might be in the sub-directory
@ -1238,9 +1237,8 @@ class TidyManager(threading.Thread):
)
if subdir_path is not None \
and os.path.isfile(subdir_path):
os.remove(subdir_path)
and os.path.isfile(subdir_path) \
and self.app_obj.remove_file(subdir_path):
self.xml_deleted_count += 1

View File

@ -156,7 +156,7 @@ class UpdateManager(threading.Thread):
Args:
cmd_list (list): Python list that contains the command to execute.
cmd_list (list): Python list that contains the command to execute
"""
@ -200,7 +200,7 @@ class UpdateManager(threading.Thread):
application with the result of the update (success or failure).
"""
# Show information about the update operation in the Output Tab
# Show information about the update operation in the Output tab
self.install_ffmpeg_write_output(
_('Starting update operation, installing FFmpeg'),
)
@ -216,7 +216,7 @@ class UpdateManager(threading.Thread):
['pacman', '-S', binary, '--noconfirm'],
)
# Show the system command in the Output Tab
# Show the system command in the Output tab
self.install_ffmpeg_write_output(
' '.join( ['pacman', '-S', binary, '--noconfirm'] ),
True, # A system command, not a message
@ -245,7 +245,7 @@ class UpdateManager(threading.Thread):
if stdout:
# Show command line output in the Output Tab (or wizard
# Show command line output in the Output tab (or wizard
# window textview)
self.install_ffmpeg_write_output(stdout)
@ -264,7 +264,7 @@ class UpdateManager(threading.Thread):
self.stderr_list.append(stderr)
# Show command line output in the Output Tab (or wizard window
# Show command line output in the Output tab (or wizard window
# textview)
self.install_ffmpeg_write_output(stderr)
@ -285,7 +285,7 @@ class UpdateManager(threading.Thread):
if not self.stderr_list:
self.success_flag = True
# Show a confirmation in the the Output Tab (or wizard window textview)
# Show a confirmation in the the Output tab (or wizard window textview)
self.install_ffmpeg_write_output(_('Update operation finished'))
# Let the timer run for a few more seconds to prevent Gtk errors (for
@ -300,7 +300,7 @@ class UpdateManager(threading.Thread):
"""Called by self.install_ffmpeg().
Writes a message to the Output Tab (or to the setup wizard window, if
Writes a message to the Output tab (or to the setup wizard window, if
called from there).
Args:
@ -308,7 +308,7 @@ class UpdateManager(threading.Thread):
msg (str): The message to display
system_cmd_flag (bool): If True, display system commands in a
different colour in the Output Tab (ignored when writing in
different colour in the Output tab (ignored when writing in
the setup wizard window)
"""
@ -343,7 +343,7 @@ class UpdateManager(threading.Thread):
application with the result of the update (success or failure).
"""
# Show information about the update operation in the Output Tab (or in
# Show information about the update operation in the Output tab (or in
# the setup wizard window, if called from there)
downloader = self.app_obj.get_downloader(self.wiz_win_obj)
self.install_ytdl_write_output(
@ -404,7 +404,7 @@ class UpdateManager(threading.Thread):
# Create a new child process using that command
self.create_child_process(mod_list)
# Show the system command in the Output Tab
# Show the system command in the Output tab
self.install_ytdl_write_output(
' '.join(mod_list),
True, # A system command, not a message
@ -452,7 +452,7 @@ class UpdateManager(threading.Thread):
self.intercept_version_from_stdout(stdout, downloader)
self.stdout_list.append(stdout)
# Show command line output in the Output Tab (or wizard
# Show command line output in the Output tab (or wizard
# window textview)
self.install_ytdl_write_output(stdout)
@ -478,7 +478,7 @@ class UpdateManager(threading.Thread):
and not re.search('You should consider upgrading', stderr):
self.stderr_list.append(stderr)
# Show command line output in the Output Tab (or wizard window
# Show command line output in the Output tab (or wizard window
# textview)
self.install_ytdl_write_output(stderr)
@ -505,7 +505,7 @@ class UpdateManager(threading.Thread):
if not self.stderr_list:
self.success_flag = True
# Show a confirmation in the the Output Tab (or wizard window textview)
# Show a confirmation in the the Output tab (or wizard window textview)
self.install_ytdl_write_output(_('Update operation finished'))
# Let the timer run for a few more seconds to prevent Gtk errors (for
@ -520,7 +520,7 @@ class UpdateManager(threading.Thread):
"""Called by self.install_ytdl().
Writes a message to the Output Tab (or to the setup wizard window, if
Writes a message to the Output tab (or to the setup wizard window, if
called from there).
Args:
@ -528,7 +528,7 @@ class UpdateManager(threading.Thread):
msg (str): The message to display
system_cmd_flag (bool): If True, display system commands in a
different colour in the Output Tab (ignored when writing in
different colour in the Output tab (ignored when writing in
the setup wizard window)
"""

View File

@ -84,7 +84,8 @@ drag_drop_text=None, no_modify_flag=None):
Returns:
The URL added to the entry (or that would have been added to the entry)
or None if no valid and non-duplicate URL was found in the clipboard
or None if no valid and non-duplicate URL was found in the
clipboard
"""
@ -263,7 +264,7 @@ def check_url(url):
Returns:
True if the URL is valid, False if invalid.
True if the URL is valid, False if invalid
"""
@ -653,11 +654,13 @@ def clip_set_destination(app_obj, video_obj):
Return values:
A list in the form
A list in the form:
(
arent_folder_object, parent_directory,
destination_folder_object, destination_directory
)
...with all values set to None if there was an error
"""
@ -694,7 +697,7 @@ def clip_set_destination(app_obj, video_obj):
)
if not os.path.isdir(dest_dir):
os.makedirs(dest_dir)
app_obj.make_directory(dest_dir)
return parent_obj, parent_dir, None, dest_dir
@ -763,7 +766,7 @@ def convert_path_to_temp(app_obj, old_path, move_flag=False):
Returns:
new_path: The converted full file path, or None if a filesystem error
Returns the converted full file path, or None if a filesystem error
occurs
"""
@ -796,18 +799,14 @@ def convert_path_to_temp(app_obj, old_path, move_flag=False):
# The destination folder must exist, before moving files into it
if not os.path.exists(new_dir):
try:
os.makedirs(new_dir)
except:
if not app_obj.make_directory(new_dir):
return None
# On MS Windows, a file name new_path must not exist, or an exception will
# be raised
if os.path.isfile(new_path):
try:
os.remove(new_path)
except:
return None
if os.path.isfile(new_path) \
and not app_obj.remove_file(new_path):
return None
# Move the file now, if the calling code requires that
if move_flag:
@ -1146,7 +1145,7 @@ def disk_get_total_space(path=None, bytes_flag=False):
Returns:
The total size in MB (or in bytes, if the flag is specified). If no
path or an invalid path is specified, returns 0
path or an invalid path is specified, returns 0
"""
@ -1186,7 +1185,7 @@ def disk_get_used_space(path=None, bytes_flag=False):
Returns:
The used space in MB (or in bytes, if the flag is specified). If no
path or an invalid path is specified, returns 0
path or an invalid path is specified, returns 0
"""
@ -1225,6 +1224,7 @@ def extract_livestream_data(stderr):
Return values:
If extraction is successful, returns a dictionary of three values:
live_msg (str): Text that can be displayed in the Video Catalogue
live_time (int): Approximate time (matching time.time()) at which
the livestream is due to start
@ -1385,9 +1385,9 @@ def fetch_slice_data(app_obj, video_obj, page_num=None, terminal_flag=False):
should be retrieved. The calling code must check that its
.vid is set
page_num (int or None): The page number of the Output Tab where
output can be displayed. If None, then no output is displayed
in the Output Tab at all; otherwise, output is displayed (or not)
page_num (int or None): The page number of the Output tab where output
can be displayed. If None, then no output is displayed in the
Output tab at all; otherwise, output is displayed (or not)
depending on the usual Tartube settings
terminal_flag (bool): If False, then no output is displayed in the
@ -1415,7 +1415,7 @@ def fetch_slice_data(app_obj, video_obj, page_num=None, terminal_flag=False):
+ short_str
payload = {}
# Write to the Output Tab and/or terminal, if required
# Write to the Output tab and/or terminal, if required
msg = '[SponsorBlock] Contacting ' + url + '...'
if page_num is not None and app_obj.ytdl_output_stdout_flag:
app_obj.main_win_obj.output_tab_write_stdout(page_num, msg)
@ -1606,7 +1606,7 @@ def find_thumbnail(app_obj, video_obj, temp_dir_flag=False):
Returns:
path (str): The full path to the thumbnail file, or None
The full path to the thumbnail file, or None
"""
@ -1697,7 +1697,7 @@ def find_thumbnail_from_filename(app_obj, dir_path, filename):
Returns:
path (str): The full path to the thumbnail file, or None
The full path to the thumbnail file, or None
"""
@ -1732,9 +1732,9 @@ def find_thumbnail_restricted(app_obj, video_obj):
Returns:
return_list (list): A list whose items, when combined, will be the full
path to the thumbnail file. If no thumbnail file was found, an
empty list is returned
A list whose items, when combined, will be the full path to the
thumbnail file. If no thumbnail file was found, an empty list is
returned
"""
@ -1774,7 +1774,7 @@ def find_thumbnail_webp(app_obj, video_obj):
Returns:
path (str): The full path to the thumbnail file, or None
The full path to the thumbnail file, or None
"""
@ -1873,7 +1873,7 @@ custom_dl_obj=None, divert_mode=None):
False if a real download is to take place
dl_classic_flag (bool): True if the download operation was launched
from the Classic Mode Tab, False otherwise
from the Classic Mode tab, False otherwise
missing_video_check_flag (bool): True if the download operation is
trying to detect missing videos (downloaded by user, but since
@ -1891,8 +1891,7 @@ custom_dl_obj=None, divert_mode=None):
Returns:
Python list that contains the system command to execute and its
arguments
A list that contains the system command to execute and its arguments
"""
@ -1941,6 +1940,12 @@ custom_dl_obj=None, divert_mode=None):
os.path.abspath(os.path.join(dl_path, 'ytdl-archive.txt')),
)
# Fetch video comments, if required
if app_obj.comment_fetch_flag \
and app_obj.ytdl_fork is not None \
and app_obj.ytdl_fork == 'yt-dlp':
options_list.append('--write-comments')
# Show verbose output (youtube-dl debugging mode), if required
if app_obj.ytdl_write_verbose_flag:
options_list.append('--verbose')
@ -2010,13 +2015,10 @@ def generate_direct_system_cmd(app_obj, media_data_obj, options_obj):
Returns:
Python list that contains the system command to execute and its
arguments
A list that contains the system command to execute and its arguments
"""
# Convert a downloader path beginning with ~ (not on MS Windows)
ytdl_path = app_obj.check_downloader(app_obj.ytdl_path)
if os.name != 'nt':
@ -2357,7 +2359,7 @@ def get_encoding():
Returns:
The system encoding.
The system encoding
"""
@ -2379,7 +2381,7 @@ def get_local_time():
Returns:
A datetime.datetime object, configured to the local time zone.
A datetime.datetime object, configured to the local time zone
"""
@ -2451,8 +2453,8 @@ def get_options_manager(app_obj, media_data_obj):
Return values:
The options.OptionsManager object that applies to the specified
media data object
The options.OptionsManager object that applies to the specified media
data object
"""
@ -2560,9 +2562,9 @@ dummy_obj=None):
if os.path.isfile(descrip_path):
if not os.path.isfile(new_path):
shutil.move(descrip_path, new_path)
app_obj.move_file_or_directory(descrip_path, new_path)
else:
os.remove(descrip_path)
app_obj.remove_file(descrip_path)
# (Don't replace a file that already exists)
elif descrip_path \
@ -2583,9 +2585,9 @@ dummy_obj=None):
if os.path.isfile(json_path):
if not os.path.isfile(new_path):
shutil.move(json_path, new_path)
app_obj.move_file_or_directory(json_path, new_path)
else:
os.remove(json_path)
app_obj.remove_file(json_path)
elif json_path \
and not os.path.isfile(json_path) \
@ -2608,9 +2610,9 @@ dummy_obj=None):
if os.path.isfile(thumb_path):
if not os.path.isfile(new_path):
shutil.move(thumb_path, new_path)
app_obj.move_file_or_directory(thumb_path, new_path)
else:
os.remove(thumb_path)
app_obj.remove_file(thumb_path)
elif thumb_path \
and not os.path.isfile(thumb_path) \
@ -2653,16 +2655,10 @@ def move_metadata_to_subdir(app_obj, video_obj, ext):
if os.path.isfile(main_path) and not os.path.isfile(subdir_path):
try:
if not os.path.isdir(subdir):
os.makedirs(subdir)
if not os.path.isdir(subdir):
app_obj.make_directory(subdir)
# (os.rename sometimes fails on external hard drives; this
# is safer)
shutil.move(main_path, subdir_path)
except:
pass
app_obj.move_file_or_directory(main_path, subdir_path)
def move_thumbnail_to_subdir(app_obj, video_obj):
@ -2704,14 +2700,10 @@ def move_thumbnail_to_subdir(app_obj, video_obj):
if os.path.isfile(main_path) \
and not os.path.isfile(subdir_path):
try:
if not os.path.isdir(subdir):
os.makedirs(subdir)
if not os.path.isdir(subdir):
app_obj.make_directory(subdir)
shutil.move(main_path, subdir_path)
except:
pass
app_obj.move_file_or_directory(main_path, subdir_path)
def open_file(app_obj, uri):
@ -2826,7 +2818,7 @@ def rename_file(app_obj, old_path, new_path):
# (On MSWin, can't do os.rename if the destination file already exists)
if os.path.isfile(new_path):
os.remove(new_path)
app_obj.remove_file(new_path)
# (os.rename sometimes fails on external hard drives; this is safer)
shutil.move(old_path, new_path)
@ -3314,7 +3306,7 @@ def timestamp_convert_to_seconds(app_obj, stamp):
Returns:
The converted value, or the original value if 'stamp' is not a valid
timestamp.
timestamp
"""

View File

@ -7,22 +7,22 @@ import shutil
import pathlib
# Yes, in THAT ORDER. See here: https://stackoverflow.com/a/61069032
import colorama
colorama.init()
#import colorama
#colorama.init()
import sys
import subprocess
def print_error(message):
# print(colorama.Fore.RED + f"[ERROR] {message}" + colorama.Style.RESET_ALL)
# print(colorama.Fore.RED + f"[ERROR] {message}" + colorama.Style.RESET_ALL)
print(f"[ERROR] {message}")
def print_warning(message):
# print(colorama.Fore.YELLOW + f"[WARNING] {message}" + colorama.Style.RESET_ALL)
# print(colorama.Fore.YELLOW + f"[WARNING] {message}" + colorama.Style.RESET_ALL)
print(f"[WARNING] {message}")
def print_info(message):
# print(colorama.Fore.CYAN + f"[INFO] {message}" + colorama.Style.RESET_ALL)
# print(colorama.Fore.CYAN + f"[INFO] {message}" + colorama.Style.RESET_ALL)
print(f"[INFO] {message}")
def sorted_alphanumeric(data):
@ -31,101 +31,101 @@ def sorted_alphanumeric(data):
return sorted(data, key=alphanum_key)
def merge_v1(audio_list, video_list, video_key, output_directory, segment_folder_name, final_export=0):
with open(output_directory / "list_{}_audio.txt".format(video_key), "w") as f:
for i in audio_list:
f.write(f"file '{i}'\n")
with open(output_directory / "list_{}_audio.txt".format(video_key), "w") as f:
for i in audio_list:
f.write(f"file '{i}'\n")
os.system("ffmpeg -loglevel panic -y -f concat -safe 0 -i \"{}\" -c:a copy \"{}\"".format(output_directory / f"list_{video_key}_audio.txt", output_directory / f"{video_key}_audio_v1_ffmpeg.m4a"))
os.remove(output_directory / f"list_{video_key}_audio.txt")
os.system("ffmpeg -loglevel panic -y -f concat -safe 0 -i \"{}\" -c:a copy \"{}\"".format(output_directory / f"list_{video_key}_audio.txt", output_directory / f"{video_key}_audio_v1_ffmpeg.m4a"))
os.remove(output_directory / f"list_{video_key}_audio.txt")
with open(output_directory / f"list_{video_key}_video.txt", "w") as f:
for i in video_list:
f.write(f"file '{i}'\n")
with open(output_directory / f"list_{video_key}_video.txt", "w") as f:
for i in video_list:
f.write(f"file '{i}'\n")
os.system("ffmpeg -loglevel panic -y -f concat -safe 0 -i \"{}\" -c:v copy \"{}\"".format(output_directory / f"list_{video_key}_video.txt", output_directory / f"{video_key}_video_v1_ffmpeg.mp4"))
os.remove(output_directory / f"list_{video_key}_video.txt")
os.system("ffmpeg -loglevel panic -y -f concat -safe 0 -i \"{}\" -c:v copy \"{}\"".format(output_directory / f"list_{video_key}_video.txt", output_directory / f"{video_key}_video_v1_ffmpeg.mp4"))
os.remove(output_directory / f"list_{video_key}_video.txt")
if final_export == 1:
final_output_filename = output_directory / f"{video_key}.mp4"
else:
final_output_filename = output_directory / f"{video_key}_v1.mp4"
if final_export == 1:
final_output_filename = output_directory / f"{video_key}.mp4"
else:
final_output_filename = output_directory / f"{video_key}_v1.mp4"
os.system("ffmpeg -loglevel panic -y -i \"{}\" -i \"{}\" -c:a copy -c:v copy \"{}\"".format(output_directory / f"{video_key}_video_v1_ffmpeg.mp4", output_directory / f"{video_key}_audio_v1_ffmpeg.m4a", final_output_filename))
os.system("ffmpeg -loglevel panic -y -i \"{}\" -i \"{}\" -c:a copy -c:v copy \"{}\"".format(output_directory / f"{video_key}_video_v1_ffmpeg.mp4", output_directory / f"{video_key}_audio_v1_ffmpeg.m4a", final_output_filename))
os.remove(output_directory / f"{video_key}_audio_v1_ffmpeg.m4a")
os.remove(output_directory / f"{video_key}_video_v1_ffmpeg.mp4")
os.remove(output_directory / f"{video_key}_audio_v1_ffmpeg.m4a")
os.remove(output_directory / f"{video_key}_video_v1_ffmpeg.mp4")
return (final_output_filename)
return (final_output_filename)
def merge_v2(audio_list, video_list, video_key, output_directory, segment_folder_name, final_export=0):
with open(output_directory / f"concat_{video_key}_audio.ts","wb") as f:
for i in audio_list:
with open(i, "rb") as ff:
shutil.copyfileobj(ff, f)
with open(output_directory / f"concat_{video_key}_audio.ts","wb") as f:
for i in audio_list:
with open(i, "rb") as ff:
shutil.copyfileobj(ff, f)
os.system("ffmpeg -loglevel panic -y -i \"{}\" -c:a copy \"{}\"".format(output_directory / f"concat_{video_key}_audio.ts", output_directory / f"{video_key}_audio_v2_ffmpeg.m4a"))
os.remove(output_directory / f"concat_{video_key}_audio.ts")
os.system("ffmpeg -loglevel panic -y -i \"{}\" -c:a copy \"{}\"".format(output_directory / f"concat_{video_key}_audio.ts", output_directory / f"{video_key}_audio_v2_ffmpeg.m4a"))
os.remove(output_directory / f"concat_{video_key}_audio.ts")
with open(output_directory / f"concat_{video_key}_video.ts","wb") as f:
for i in video_list:
with open(i, "rb") as ff:
shutil.copyfileobj(ff, f)
with open(output_directory / f"concat_{video_key}_video.ts","wb") as f:
for i in video_list:
with open(i, "rb") as ff:
shutil.copyfileobj(ff, f)
os.system("ffmpeg -loglevel panic -y -i \"{}\" -c:v copy \"{}\"".format(output_directory / f"concat_{video_key}_video.ts", output_directory / f"{video_key}_video_v2_ffmpeg.mp4"))
os.remove(output_directory / f"concat_{video_key}_video.ts")
os.system("ffmpeg -loglevel panic -y -i \"{}\" -c:v copy \"{}\"".format(output_directory / f"concat_{video_key}_video.ts", output_directory / f"{video_key}_video_v2_ffmpeg.mp4"))
os.remove(output_directory / f"concat_{video_key}_video.ts")
if final_export == 1:
final_output_filename = output_directory / f"{video_key}.mp4"
else:
final_output_filename = output_directory / f"{video_key}_v2.mp4"
if final_export == 1:
final_output_filename = output_directory / f"{video_key}.mp4"
else:
final_output_filename = output_directory / f"{video_key}_v2.mp4"
os.system("ffmpeg -loglevel panic -y -i \"{}\" -i \"{}\" -c:a copy -c:v copy \"{}\"".format(output_directory / f"{video_key}_audio_v2_ffmpeg.m4a", output_directory / f"{video_key}_video_v2_ffmpeg.mp4", output_directory / f"{video_key}_v2.mp4"))
os.remove(output_directory / f"{video_key}_audio_v2_ffmpeg.m4a")
os.remove(output_directory / f"{video_key}_video_v2_ffmpeg.mp4")
os.system("ffmpeg -loglevel panic -y -i \"{}\" -i \"{}\" -c:a copy -c:v copy \"{}\"".format(output_directory / f"{video_key}_audio_v2_ffmpeg.m4a", output_directory / f"{video_key}_video_v2_ffmpeg.mp4", output_directory / f"{video_key}_v2.mp4"))
os.remove(output_directory / f"{video_key}_audio_v2_ffmpeg.m4a")
os.remove(output_directory / f"{video_key}_video_v2_ffmpeg.mp4")
return (final_output_filename)
return (final_output_filename)
args = sys.argv
output_directory = ""
for index, element in enumerate(args):
if '?v=' in element:
video_key = element.split('?v=')[1]
if '&' in video_key:
video_key = video_key.split('&')[0]
segment_folder_name = f"segments_{video_key}"
if '?v=' in element:
video_key = element.split('?v=')[1]
if '&' in video_key:
video_key = video_key.split('&')[0]
segment_folder_name = f"segments_{video_key}"
if '--output-directory' == element:
try:
output_directory = pathlib.Path(args[index + 1])
output_directory = output_directory.absolute()
if not output_directory.exists():
print_warning("Output directory does not exist, defaulting to the root directory of the script...")
output_directory = ""
else:
print_info(f"Set output directory to {output_directory}")
except Exception as e:
print(e)
print_warning("Output directory could not be set, defaulting to the root directory of the script...")
output_directory = ""
if '--output-directory' == element:
try:
output_directory = pathlib.Path(args[index + 1])
output_directory = output_directory.absolute()
if not output_directory.exists():
print_warning("Output directory does not exist, defaulting to the root directory of the script...")
output_directory = ""
else:
print_info(f"Set output directory to {output_directory}")
except Exception as e:
print(e)
print_warning("Output directory could not be set, defaulting to the root directory of the script...")
output_directory = ""
if video_key == "":
print_error("No URL given! Exiting now...")
exit()
print_error("No URL given! Exiting now...")
exit()
# Create folder in root if no output path given
if output_directory == "":
output_directory = pathlib.Path.cwd()
output_directory = pathlib.Path.cwd()
if not pathlib.Path.is_dir(output_directory / segment_folder_name):
print_error(f"Directory with stream segments is missing from {output_directory}!")
print_error(f"Expected directory to be present at {output_directory / segment_folder_name}! Exiting now...")
exit()
print_error(f"Directory with stream segments is missing from {output_directory}!")
print_error(f"Expected directory to be present at {output_directory / segment_folder_name}! Exiting now...")
exit()
print_info("Checking available segments...")
dirlist = sorted_alphanumeric([x.name for x in pathlib.Path(output_directory / segment_folder_name).glob('*.ts')])
@ -141,49 +141,49 @@ total_segment_list_sorted_video = []
total_segment_list_sorted_audio = []
for f in range(first_segment, last_segment):
if not pathlib.Path(output_directory / segment_folder_name / f"{f}_{video_key}_video.ts").is_file():
print_warning("Missing segment: {}".format(output_directory / segment_folder_name / f"{f}_{video_key}_video.ts"))
missing_files = True
elif not pathlib.Path(output_directory / segment_folder_name / f"{f}_{video_key}_audio.ts").is_file():
print_warning("Missing segment: {}".format(output_directory / segment_folder_name / f"{f}_{video_key}_audio.ts"))
missing_files = True
else:
total_segment_list_sorted_audio.append(output_directory / segment_folder_name / f"{f}_{video_key}_audio.ts")
total_segment_list_sorted_video.append(output_directory / segment_folder_name / f"{f}_{video_key}_video.ts")
if not pathlib.Path(output_directory / segment_folder_name / f"{f}_{video_key}_video.ts").is_file():
print_warning("Missing segment: {}".format(output_directory / segment_folder_name / f"{f}_{video_key}_video.ts"))
missing_files = True
elif not pathlib.Path(output_directory / segment_folder_name / f"{f}_{video_key}_audio.ts").is_file():
print_warning("Missing segment: {}".format(output_directory / segment_folder_name / f"{f}_{video_key}_audio.ts"))
missing_files = True
else:
total_segment_list_sorted_audio.append(output_directory / segment_folder_name / f"{f}_{video_key}_audio.ts")
total_segment_list_sorted_video.append(output_directory / segment_folder_name / f"{f}_{video_key}_video.ts")
if missing_files == True:
print_warning("There were missing segments! Merged output might might noticeably skip at certain points...")
print_warning("There were missing segments! Merged output might might noticeably skip at certain points...")
else:
print_info("No missing segments!")
print_info("No missing segments!")
print_info("Analyzing files...")
cmd = " ".join(['ffprobe', '-v', 'quiet', '-hide_banner', '-show_streams','\"{}\"'.format(total_segment_list_sorted_video[0]), '2>&1'])
ffprobe_output = os.popen(cmd).read().split('\n')
for line in ffprobe_output:
if 'codec_tag_string' in line:
codec_tag_string = line.split("=")[1]
print_info(line)
if 'codec_name' in line:
codec_name = line.split("=")[1]
print_info(line)
if 'r_frame_rate' in line:
r_frame_rate = line.split("=")[1]
print_info(line)
if 'height=' in line:
height = line.split("=")[1]
print_info(line)
if 'codec_tag_string' in line:
codec_tag_string = line.split("=")[1]
print_info(line)
if 'codec_name' in line:
codec_name = line.split("=")[1]
print_info(line)
if 'r_frame_rate' in line:
r_frame_rate = line.split("=")[1]
print_info(line)
if 'height=' in line:
height = line.split("=")[1]
print_info(line)
merge_v1_test_segments_audio = []
merge_v1_test_segments_video = []
print_info("Testing merge method 1 / 2...")
if (len(total_segment_list_sorted_video) + len(total_segment_list_sorted_audio)) >= 200:
merge_v1_test_segments_audio = total_segment_list_sorted_audio[:100]
merge_v1_test_segments_video = total_segment_list_sorted_video[:100]
merge_v1_test_segments_audio = total_segment_list_sorted_audio[:100]
merge_v1_test_segments_video = total_segment_list_sorted_video[:100]
else:
merge_v1_test_segments_audio = total_segment_list_sorted_audio
merge_v1_test_segments_video = total_segment_list_sorted_video
merge_v1_test_segments_audio = total_segment_list_sorted_audio
merge_v1_test_segments_video = total_segment_list_sorted_video
v1_test_path = merge_v1(merge_v1_test_segments_audio, merge_v1_test_segments_video, video_key, output_directory, segment_folder_name)
@ -193,11 +193,11 @@ merge_v2_test_segments_video = []
print_info("Testing merge method 2 / 2...")
if (len(total_segment_list_sorted_video) + len(total_segment_list_sorted_audio)) >= 200:
merge_v2_test_segments_audio = total_segment_list_sorted_audio[:100]
merge_v2_test_segments_video = total_segment_list_sorted_video[:100]
merge_v2_test_segments_audio = total_segment_list_sorted_audio[:100]
merge_v2_test_segments_video = total_segment_list_sorted_video[:100]
else:
merge_v2_test_segments_audio = total_segment_list_sorted_audio
merge_v2_test_segments_video = total_segment_list_sorted_video
merge_v2_test_segments_audio = total_segment_list_sorted_audio
merge_v2_test_segments_video = total_segment_list_sorted_video
v2_test_path = merge_v2(merge_v1_test_segments_audio, merge_v1_test_segments_video, video_key, output_directory, segment_folder_name)
@ -207,9 +207,9 @@ cmd = " ".join(['ffprobe', '-v', 'quiet', '-hide_banner', '-show_streams','\"{}\
ffprobe_output = os.popen(cmd).read().split('\n')
for line in ffprobe_output:
if 'duration=' in line:
duration_v1 = float(line.split("=")[1])
break
if 'duration=' in line:
duration_v1 = float(line.split("=")[1])
break
print_info(line)
@ -218,9 +218,9 @@ cmd = " ".join(['ffprobe', '-v', 'quiet', '-hide_banner', '-show_streams','\"{}\
ffprobe_output = os.popen(cmd).read().split('\n')
for line in ffprobe_output:
if 'duration=' in line:
duration_v2 = float(line.split("=")[1])
break
if 'duration=' in line:
duration_v2 = float(line.split("=")[1])
break
print_info(line)
@ -231,20 +231,20 @@ os.remove(v1_test_path)
os.remove(v2_test_path)
if duration_v1 < (len(merge_v1_test_segments_audio) * 0.8) or (duration_v1 > 20 * len(merge_v1_test_segments_audio)):
print_warning("File of method 1 broken.")
f1_working = False
print_warning("File of method 1 broken.")
f1_working = False
if duration_v2 < (len(merge_v2_test_segments_audio) * 0.8) or (duration_v2 > 20 * len(merge_v2_test_segments_audio)):
print_warning("File of method 2 broken.")
f2_working = False
print_warning("File of method 2 broken.")
f2_working = False
if f1_working:
print_info("Using method 1 for this livestream. This process might take a while...")
v1_path = merge_v1(total_segment_list_sorted_audio, total_segment_list_sorted_video, video_key, output_directory, segment_folder_name, 1)
print("Output file: {}".format(v1_path))
print_info("Using method 1 for this livestream. This process might take a while...")
v1_path = merge_v1(total_segment_list_sorted_audio, total_segment_list_sorted_video, video_key, output_directory, segment_folder_name, 1)
print("Output file: {}".format(v1_path))
elif f2_working:
print_info("using method 2 for this livestream. This process might take a while...")
v2_path = merge_v2(total_segment_list_sorted_audio, total_segment_list_sorted_video, video_key, output_directory, segment_folder_name, 1)
print("Output file: {}".format(v2_path))
print_info("using method 2 for this livestream. This process might take a while...")
v2_path = merge_v2(total_segment_list_sorted_audio, total_segment_list_sorted_video, video_key, output_directory, segment_folder_name, 1)
print("Output file: {}".format(v2_path))
else:
print_error("Both methods aren't working for some reason. Don't delete the recording just yet! If you're sure that you didn't mess with the files in any way, please open an issue on Github and report this.")
print_error("Both methods aren't working for some reason. Don't delete the recording just yet! If you're sure that you didn't mess with the files in any way, please open an issue on Github and report this.")

View File

@ -4,11 +4,13 @@ import re
import os
# Yes, in that order.
import colorama
colorama.init()
#import colorama
#colorama.init()
import sys
import random
import json
import time
import requests
@ -34,14 +36,14 @@ def print_info(message):
def parse_cookie_file(cookiefile):
cookies = {}
with open (cookiefile, 'r') as fp:
content = fp.read()
for line in content.split('\n'):
if 'youtube' in line:
elements = line.split('\t')
cookies[elements[5]] = elements[6]
return cookies
cookies = {}
with open (cookiefile, 'r') as fp:
content = fp.read()
for line in content.split('\n'):
if 'youtube' in line:
elements = line.split('\t')
cookies[elements[5]] = elements[6]
return cookies
audio_base_url = ""
video_base_url = ""
@ -53,30 +55,30 @@ video_lmt_number = 0
audio_lmt_number = 0
quality_video_ranking = [
402, 138, # 4320p: AV1 HFR | VP9 HFR | H.264
401, 266, # 2160p: AV1 HFR | VP9.2 HDR HFR | VP9 HFR | VP9 | H.264
400, 264, # 1440p: AV1 HFR | VP9.2 HDR HFR | VP9 HFR | VP9 | H.264
399, 299, 137, # 1080p: AV1 HFR | VP9.2 HDR HFR | VP9 HFR | VP9 | H.264 HFR | H.264
398, 298, 136, # 720p: AV1 HFR | VP9.2 HDR HFR | VP9 HFR | VP9 | H.264 HFR | H.264
397, 135, # 480p: AV1 | VP9.2 HDR HFR | VP9 | H.264
396, 134, # 360p: AV1 | VP9.2 HDR HFR | VP9 | H.264
395, 133, # 240p: AV1 | VP9.2 HDR HFR | VP9 | H.264
394, 160 # 144p: AV1 | VP9.2 HDR HFR | VP9 | H.264
]
402, 138, # 4320p: AV1 HFR | VP9 HFR | H.264
401, 266, # 2160p: AV1 HFR | VP9.2 HDR HFR | VP9 HFR | VP9 | H.264
400, 264, # 1440p: AV1 HFR | VP9.2 HDR HFR | VP9 HFR | VP9 | H.264
399, 299, 137, # 1080p: AV1 HFR | VP9.2 HDR HFR | VP9 HFR | VP9 | H.264 HFR | H.264
398, 298, 136, # 720p: AV1 HFR | VP9.2 HDR HFR | VP9 HFR | VP9 | H.264 HFR | H.264
397, 135, # 480p: AV1 | VP9.2 HDR HFR | VP9 | H.264
396, 134, # 360p: AV1 | VP9.2 HDR HFR | VP9 | H.264
395, 133, # 240p: AV1 | VP9.2 HDR HFR | VP9 | H.264
394, 160 # 144p: AV1 | VP9.2 HDR HFR | VP9 | H.264
]
quality_audio_ranking = [140]
# Experimental - VP9 support
# quality_video_ranking = [
# 402, 272, 138, # 4320p: AV1 HFR | VP9 HFR | H.264
# 401, 337, 315, 313, 266, # 2160p: AV1 HFR | VP9.2 HDR HFR | VP9 HFR | VP9 | H.264
# 400, 336, 308, 271, 264, # 1440p: AV1 HFR | VP9.2 HDR HFR | VP9 HFR | VP9 | H.264
# 399, 335, 303, 248, 299, 137, # 1080p: AV1 HFR | VP9.2 HDR HFR | VP9 HFR | VP9 | H.264 HFR | H.264
# 398, 334, 302, 247, 298, 136, # 720p: AV1 HFR | VP9.2 HDR HFR | VP9 HFR | VP9 | H.264 HFR | H.264
# 397, 333, 244, 135, # 480p: AV1 | VP9.2 HDR HFR | VP9 | H.264
# 396, 332, 243, 134, # 360p: AV1 | VP9.2 HDR HFR | VP9 | H.264
# 395, 331, 242, 133, # 240p: AV1 | VP9.2 HDR HFR | VP9 | H.264
# 394, 330, 278, 160 # 144p: AV1 | VP9.2 HDR HFR | VP9 | H.264
# ]
# 402, 272, 138, # 4320p: AV1 HFR | VP9 HFR | H.264
# 401, 337, 315, 313, 266, # 2160p: AV1 HFR | VP9.2 HDR HFR | VP9 HFR | VP9 | H.264
# 400, 336, 308, 271, 264, # 1440p: AV1 HFR | VP9.2 HDR HFR | VP9 HFR | VP9 | H.264
# 399, 335, 303, 248, 299, 137, # 1080p: AV1 HFR | VP9.2 HDR HFR | VP9 HFR | VP9 | H.264 HFR | H.264
# 398, 334, 302, 247, 298, 136, # 720p: AV1 HFR | VP9.2 HDR HFR | VP9 HFR | VP9 | H.264 HFR | H.264
# 397, 333, 244, 135, # 480p: AV1 | VP9.2 HDR HFR | VP9 | H.264
# 396, 332, 243, 134, # 360p: AV1 | VP9.2 HDR HFR | VP9 | H.264
# 395, 331, 242, 133, # 240p: AV1 | VP9.2 HDR HFR | VP9 | H.264
# 394, 330, 278, 160 # 144p: AV1 | VP9.2 HDR HFR | VP9 | H.264
# ]
# quality_audio_ranking = [251,250,249,172,171,141,140,139]
@ -89,383 +91,419 @@ cookie_content = {}
# Argument parsing
for index, element in enumerate(args):
# Get video key
if '?v=' in element:
folder_suffix = element.split('?v=')[1]
if '&' in folder_suffix:
folder_suffix = folder_suffix.split('&')[0]
# Get video key
if '?v=' in element:
folder_suffix = element.split('?v=')[1]
if '&' in folder_suffix:
folder_suffix = folder_suffix.split('&')[0]
segment_folder_name = f"segments_{folder_suffix}"
segment_folder_name = f"segments_{folder_suffix}"
if '--start-segment' == element:
try:
segment_number = int(args[index + 1])
except:
print_warning("Failed to get segment number, setting it to 0!")
segment_number = 0
if '--start-segment' == element:
try:
segment_number = int(args[index + 1])
except:
print_warning("Failed to get segment number, setting it to 0!")
segment_number = 0
if '--output-directory' == element:
try:
output_directory = pathlib.Path(args[index + 1])
output_directory = output_directory.absolute()
if not output_directory.exists():
print_warning("Output directory does not exist, defaulting to the root directory of the script...")
output_directory = ""
else:
print_info(f"Set output directory to {output_directory}")
except Exception as e:
print(e)
print_warning("Output directory could not be set, defaulting to the root directory of the script...")
output_directory = ""
if '--output-directory' == element:
try:
output_directory = pathlib.Path(args[index + 1])
output_directory = output_directory.absolute()
if not output_directory.exists():
print_warning("Output directory does not exist, defaulting to the root directory of the script...")
output_directory = ""
else:
print_info(f"Set output directory to {output_directory}")
except Exception as e:
print(e)
print_warning("Output directory could not be set, defaulting to the root directory of the script...")
output_directory = ""
if '--cookie-file' == element:
try:
cookie_path = pathlib.Path(args[index + 1]).absolute()
if not cookie_path.exists():
print_error("Cookie file does not exist, defaulting to empty cookie...")
cookie_content = {}
else:
print_info(f"Found cookie at {cookie_path}")
cookie_content = parse_cookie_file(cookie_path)
if cookie_content == {}:
print_info("Empty cookie!")
else:
print_info(f"Cookie: {cookie_content}")
if '--cookie-file' == element:
try:
cookie_path = pathlib.Path(args[index + 1]).absolute()
if not cookie_path.exists():
print_error("Cookie file does not exist, defaulting to empty cookie...")
cookie_content = {}
else:
print_info(f"Found cookie at {cookie_path}")
cookie_content = parse_cookie_file(cookie_path)
if cookie_content == {}:
print_info("Empty cookie!")
else:
print_info(f"Cookie: {cookie_content}")
except:
print_error("Could not parse cookie, defaulting to empty cookie...")
cookie_content = {}
except:
print_error("Could not parse cookie, defaulting to empty cookie...")
cookie_content = {}
if folder_suffix == "":
print_error("No stream link given! Exiting now...")
exit()
print_error("No stream link given! Exiting now...")
exit()
startTime = datetime.now()
# Create folder in root if no output path given
if output_directory == "":
output_directory = pathlib.Path.cwd() / segment_folder_name
if not pathlib.Path.is_dir(output_directory):
pathlib.Path.mkdir(output_directory)
print_info(f"Created directory {output_directory}")
output_directory = pathlib.Path.cwd() / segment_folder_name
if not pathlib.Path.is_dir(output_directory):
pathlib.Path.mkdir(output_directory)
print_info(f"Created directory {output_directory}")
else:
output_directory = output_directory / segment_folder_name
if not pathlib.Path.is_dir(output_directory):
pathlib.Path.mkdir(output_directory)
print_info(f"Created directory {output_directory}")
output_directory = output_directory / segment_folder_name
if not pathlib.Path.is_dir(output_directory):
pathlib.Path.mkdir(output_directory)
print_info(f"Created directory {output_directory}")
# Could I just have used an already existing mpeg-dash parser? Probably.
# Did the one I could find have any documentation?
# No.
def get_segment_list(dash_content, itag):
global audio_base_url
global audio_lmt_number
global audio_lmt_distance
global quality_audio_ranking
global audio_base_url
global audio_lmt_number
global audio_lmt_distance
global quality_audio_ranking
global video_base_url
global video_lmt_number
global video_lmt_distance
global quality_video_ranking
global video_base_url
global video_lmt_number
global video_lmt_distance
global quality_video_ranking
if itag in quality_video_ranking:
video_base_url = dash_content.split("<Representation id=\"{}\"".format(itag))[1]
video_base_url = video_base_url.split("</BaseURL>")[0]
video_base_url = video_base_url.split("<BaseURL>")[1]
if itag in quality_video_ranking:
video_base_url = dash_content.split("<Representation id=\"{}\"".format(itag))[1]
video_base_url = video_base_url.split("</BaseURL>")[0]
video_base_url = video_base_url.split("<BaseURL>")[1]
segment_list = []
segment_list = []
for i in range(0, 999999):
try:
video_segment_part = dash_content.split("<Representation id=\"{}\"".format(itag))[1]
video_segment_part = video_segment_part.split("</SegmentList>")[0]
video_segment_part = video_segment_part.split("sq/{}/".format(i))[1]
first_segment = i
video_segment_part = "sq/{}/".format(i) + video_segment_part.split("\"/>")[0]
break
except:
pass
last_segment = 0
for i in range(i, 999999):
try:
((dash_content.split("<Representation id=\"{}\"".format(itag))[1]).split("</SegmentList>")[0]).split("sq/{}/".format(i))[1]
except:
break
last_segment = i
for i in range(0, 999999):
try:
video_segment_part = dash_content.split("<Representation id=\"{}\"".format(itag))[1]
video_segment_part = video_segment_part.split("</SegmentList>")[0]
video_segment_part = video_segment_part.split("sq/{}/".format(i))[1]
first_segment = i
video_segment_part = "sq/{}/".format(i) + video_segment_part.split("\"/>")[0]
break
except:
pass
last_segment = 0
for i in range(i, 999999):
try:
((dash_content.split("<Representation id=\"{}\"".format(itag))[1]).split("</SegmentList>")[0]).split("sq/{}/".format(i))[1]
except:
break
last_segment = i
video_lmt_number = 0
video_lmt_distance = 0
video_lmt_number = 0
video_lmt_distance = 0
for j in range(first_segment, first_segment + 10):
try:
number = dash_content.split("<Representation id=\"{}\"".format(itag))[1]
number = number.split("</SegmentList>")[0]
number = number.split("sq/{}/".format(j))[1]
number = number.split("/")[1]
number = number.split("\"/>")[0][:-1]
for j in range(first_segment, first_segment + 10):
try:
number = dash_content.split("<Representation id=\"{}\"".format(itag))[1]
number = number.split("</SegmentList>")[0]
number = number.split("sq/{}/".format(j))[1]
number = number.split("/")[1]
number = number.split("\"/>")[0][:-1]
if(video_lmt_number == 0):
video_lmt_number = int(number)
if int(number) - video_lmt_number > 0:
video_lmt_distance = int(number) - video_lmt_number
break
except:
pass
if(video_lmt_number == 0):
video_lmt_number = int(number)
if int(number) - video_lmt_number > 0:
video_lmt_distance = int(number) - video_lmt_number
break
except:
pass
for k in (range(0, last_segment)):
segment_list.append("{}sq/{}/{}".format(video_base_url, k, video_lmt_number - (video_lmt_distance * (last_segment - k))))
for k in (range(0, last_segment)):
segment_list.append("{}sq/{}/{}".format(video_base_url, k, video_lmt_number - (video_lmt_distance * (last_segment - k))))
return segment_list
return segment_list
if itag in quality_audio_ranking:
audio_base_url = dash_content.split("<Representation id=\"{}\"".format(itag))[1]
audio_base_url = audio_base_url.split("</BaseURL>")[0]
audio_base_url = audio_base_url.split("<BaseURL>")[1]
if itag in quality_audio_ranking:
audio_base_url = dash_content.split("<Representation id=\"{}\"".format(itag))[1]
audio_base_url = audio_base_url.split("</BaseURL>")[0]
audio_base_url = audio_base_url.split("<BaseURL>")[1]
segment_list = []
segment_list = []
for i in range(0, 999999):
try:
audio_segment_part = dash_content.split("<Representation id=\"{}\"".format(itag))[1]
audio_segment_part = audio_segment_part.split("</SegmentList>")[0]
audio_segment_part = audio_segment_part.split("sq/{}/".format(i))[1]
first_segment = i
audio_segment_part = "sq/{}/".format(i) + audio_segment_part.split("\"/>")[0]
break
except:
pass
last_segment = 0
for i in range(i, 999999):
try:
((dash_content.split("<Representation id=\"{}\"".format(itag))[1]).split("</SegmentList>")[0]).split("sq/{}/".format(i))[1]
except:
break
last_segment = i
for i in range(0, 999999):
try:
audio_segment_part = dash_content.split("<Representation id=\"{}\"".format(itag))[1]
audio_segment_part = audio_segment_part.split("</SegmentList>")[0]
audio_segment_part = audio_segment_part.split("sq/{}/".format(i))[1]
first_segment = i
audio_segment_part = "sq/{}/".format(i) + audio_segment_part.split("\"/>")[0]
break
except:
pass
last_segment = 0
for i in range(i, 999999):
try:
((dash_content.split("<Representation id=\"{}\"".format(itag))[1]).split("</SegmentList>")[0]).split("sq/{}/".format(i))[1]
except:
break
last_segment = i
audio_lmt_number = 0
audio_lmt_distance = 0
audio_lmt_number = 0
audio_lmt_distance = 0
for j in range(first_segment, first_segment + 10):
try:
number = dash_content.split("<Representation id=\"{}\"".format(itag))[1]
number = number.split("</SegmentList>")[0]
number = number.split("sq/{}/".format(j))[1]
number = number.split("/")[1]
number = number.split("\"/>")[0][:-1]
for j in range(first_segment, first_segment + 10):
try:
number = dash_content.split("<Representation id=\"{}\"".format(itag))[1]
number = number.split("</SegmentList>")[0]
number = number.split("sq/{}/".format(j))[1]
number = number.split("/")[1]
number = number.split("\"/>")[0][:-1]
if(audio_lmt_number == 0):
audio_lmt_number = int(number)
if int(number) - audio_lmt_number > 0:
audio_lmt_distance = int(number) - audio_lmt_number
break
except:
pass
if(audio_lmt_number == 0):
audio_lmt_number = int(number)
if int(number) - audio_lmt_number > 0:
audio_lmt_distance = int(number) - audio_lmt_number
break
except:
pass
for k in (range(0, last_segment)):
segment_list.append("{}sq/{}/{}".format(audio_base_url, k, audio_lmt_number - (audio_lmt_distance * (last_segment - k))))
for k in (range(0, last_segment)):
segment_list.append("{}sq/{}/{}".format(audio_base_url, k, audio_lmt_number - (audio_lmt_distance * (last_segment - k))))
return segment_list
return segment_list
def get_new_segment(dash_content, itag, old_segment_number):
global video_base_url
global video_lmt_distance
global video_lmt_number
global video_base_url
global video_lmt_distance
global video_lmt_number
global audio_base_url
global audio_lmt_distance
global audio_lmt_number
global audio_base_url
global audio_lmt_distance
global audio_lmt_number
global quality_audio_ranking
global quality_video_ranking
global quality_audio_ranking
global quality_video_ranking
if itag in quality_audio_ranking:
return "{}sq/{}/{}".format(audio_base_url, old_segment_number, audio_lmt_number - (audio_lmt_distance * (old_segment_number)))
if itag in quality_video_ranking:
return "{}sq/{}/{}".format(video_base_url, old_segment_number, video_lmt_number - (video_lmt_distance * (old_segment_number)))
if itag in quality_audio_ranking:
return "{}sq/{}/{}".format(audio_base_url, old_segment_number, audio_lmt_number - (audio_lmt_distance * (old_segment_number)))
if itag in quality_video_ranking:
return "{}sq/{}/{}".format(video_base_url, old_segment_number, video_lmt_number - (video_lmt_distance * (old_segment_number)))
def run_script():
global output_directory
global segment_number
global dash_tries
global cookie_content
req = requests.get(sys.argv[1])
headers = {
'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.138 Safari/537.36'
}
global output_directory
global segment_number
global dash_tries
global cookie_content
req = requests.get(sys.argv[1], headers=headers, cookies=cookie_content)
print("Status code: {}".format(req.status_code))
if req.status_code == 429:
print_error("Too many requests. Please try again later (or get yourself another IP, I don't make the rules).")
print_error("You might also just need to get yourself a new cookie. I'm not YouTube, what do I know?")
return -1
content_page = req.text
content_page = content_page.split("ytInitialPlayerResponse = ")
content_page = content_page[1]
content_page = content_page.split(";var meta = document.")
content_page = content_page[0]
# Credit to youtube-dl for this:
# https://github.com/ytdl-org/youtube-dl/commit/14f29f087e6097feb46bdb84878924bc410a57eb
filename_thing = sys.argv[1].split('?v=')
filename_thing = filename_thing[1]
try:
j = json.loads(content_page)
except Exception as e:
print(e)
s = requests.Session()
x = j
# If we didn't explicitly set a cookie
# we might have to bypass the consent screen
for el in x['responseContext']['serviceTrackingParams']:
for i in el['params']:
if i['key'] == 'cver':
cver_string = i['value']
if cookie_content == {}:
s.get("https://www.youtube.com/")
if s.cookies.get("__Secure-3PSID") is None:
# We need to do the consent check thing
consent_id = None
consent = s.cookies.get("CONSENT")
# Select the best possible quality
quality_video_ids = []
quality_audio_ids = []
if consent:
print(consent)
if "YES" in consent:
pass
else:
consent_id = re.findall(r'PENDING\+(\d+)', consent)
if len(consent_id) > 0:
consent_id = consent_id[0]
else:
consent_id = random.randint(100,999)
global quality_video_ranking
global quality_audio_ranking
del s.cookies["CONSENT"]
s.cookies.set("CONSENT",f"YES+cb.20210328-17-p0.en+FX+{consent_id}", domain="youtube.com")
for i in range(len(x['streamingData']['adaptiveFormats'])):
try:
if x['streamingData']['adaptiveFormats'][i]['qualityLabel'] is not None:
quality_video_ids.append(x['streamingData']['adaptiveFormats'][i]['itag'])
except:
pass
for i in range(len(x['streamingData']['adaptiveFormats'])):
try:
if x['streamingData']['adaptiveFormats'][i]['audioQuality'] is not None:
quality_audio_ids.append(x['streamingData']['adaptiveFormats'][i]['itag'])
except:
pass
if cookie_content == {}:
req = s.get(sys.argv[1])
else:
headers = {
'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.138 Safari/537.36'
}
req = requests.get(sys.argv[1], headers=headers, cookies=cookie_content)
for i in quality_video_ranking:
if i in quality_video_ids:
chosen_quality_video = i
break
print("Status code: {}".format(req.status_code))
if req.status_code == 429:
print_error("Too many requests. Please try again later (or get yourself another IP, I don't make the rules).")
print_error("You might also just need to get yourself a new cookie. I'm not YouTube, what do I know?")
return -1
for i in quality_audio_ranking:
if i in quality_audio_ids:
chosen_quality_audio = i
break
content_page = req.text
content_page = content_page.split("ytInitialPlayerResponse = ")
content_page = content_page[1]
content_page = content_page.split(";</script>")
content_page = content_page[0]
content_page = content_page.split(';var')
content_page = content_page[0]
print("Chosen video quality: {}".format(chosen_quality_video))
print("Chose audio quality: {}".format(chosen_quality_audio))
filename_thing = sys.argv[1].split('?v=')
filename_thing = filename_thing[1]
try:
j = json.loads(content_page)
except Exception as e:
print(e)
dash_url = x['streamingData']['dashManifestUrl']
r = requests.get(dash_url).text
x = j
dash_content = requests.get(dash_url).text
global tries
global retries
video_segment_list = []
for el in x['responseContext']['serviceTrackingParams']:
for i in el['params']:
if i['key'] == 'cver':
cver_string = i['value']
session = requests.Session()
biggest_segment = 0
# Select the best possible quality
quality_video_ids = []
quality_audio_ids = []
video_segment_list = get_segment_list(dash_content, chosen_quality_video)
audio_segment_list = get_segment_list(dash_content, chosen_quality_audio)
global quality_video_ranking
global quality_audio_ranking
for i in range(segment_number, 999999):
i = segment_number
try:
for i in range(len(x['streamingData']['adaptiveFormats'])):
try:
if x['streamingData']['adaptiveFormats'][i]['qualityLabel'] is not None:
quality_video_ids.append(x['streamingData']['adaptiveFormats'][i]['itag'])
except:
pass
print("Segment number: {}".format(segment_number))
print(f"Total number of segments: {len(video_segment_list)}")
if segment_number > (len(video_segment_list)):
dash_content = requests.get(dash_url).text
time.sleep(50)
segment_number = segment_number - 5
continue
# Segment might not be in list yet
except:
print("Exception")
dash_content = requests.get(dash_url).text
time.sleep(10)
segment_number = segment_number - 5
continue
for i in range(len(x['streamingData']['adaptiveFormats'])):
try:
if x['streamingData']['adaptiveFormats'][i]['audioQuality'] is not None:
quality_audio_ids.append(x['streamingData']['adaptiveFormats'][i]['itag'])
except:
pass
print(f"URL: {video_segment_list[segment_number]}")
if segment_number < len(video_segment_list):
for i in quality_video_ranking:
if i in quality_video_ids:
chosen_quality_video = i
break
while(True):
try:
if(dash_tries == retries):
print("Exceeded {} retries! Exiting...".format(retries))
return -1
for i in quality_audio_ranking:
if i in quality_audio_ids:
chosen_quality_audio = i
break
r = session.head(video_segment_list[segment_number])
if r.status_code == 200:
break
dash_tries += 1
time.sleep(4)
except:
dash_tries += 1
time.sleep(4)
print("Chosen video quality: {}".format(chosen_quality_video))
print("Chose audio quality: {}".format(chosen_quality_audio))
os.system("aria2c -c --auto-file-renaming=false --max-tries=100 --retry-wait=5 -j 3 -x 3 -s 3 -k 1M \"{}\" -d \"{}\" -o \"{}\"".format(video_segment_list[segment_number], output_directory, f"{segment_number}_{filename_thing}_video.ts"))
while(True):
try:
r = session.head(audio_segment_list[segment_number])
if r.status_code == 200:
break
time.sleep(2)
except:
time.sleep(2)
dash_url = x['streamingData']['dashManifestUrl']
r = requests.get(dash_url).text
os.system("aria2c -c --auto-file-renaming=false --max-tries=100 --retry-wait=5 -j 3 -x 3 -s 3 -k 1M \"{}\" -d \"{}\" -o \"{}\"".format(audio_segment_list[segment_number], output_directory, f"{segment_number}_{filename_thing}_audio.ts"))
try:
if pathlib.Path(output_directory / f"{segment_number}_{filename_thing}_video.ts").stat().st_size < 2000 or pathlib.Path(output_directory / f"{segment_number}_{filename_thing}_audio.ts").stat().st_size < 2000:
segment_number -= 4
print("Trying again!")
continue
else:
# It worked!
segment_number += 1
global startTime
print("Time since last reset: {}".format(datetime.now() - startTime))
except:
segment_number -= 3
print("Trying again!")
continue
dash_content = requests.get(dash_url).text
global tries
global retries
video_segment_list = []
if segment_number > biggest_segment:
dash_tries = 0
biggest_segment = segment_number
else:
dash_tries += 1
session = requests.Session()
biggest_segment = 0
if segment_number >= len(video_segment_list):
print("Tries: {}".format(dash_tries))
# time.sleep(1)
dash_content = requests.get(dash_url).text
video_segment_list = get_segment_list(dash_content, chosen_quality_video)
audio_segment_list = get_segment_list(dash_content, chosen_quality_audio)
video_segment_list.append(get_new_segment(dash_content, int(chosen_quality_video), segment_number))
audio_segment_list.append(get_new_segment(dash_content, int(chosen_quality_audio), segment_number))
for i in range(segment_number, 999999):
i = segment_number
try:
if dash_tries == retries:
print("Exceeded {} retries! Exiting...".format(retries))
return -1
if ((datetime.now() - startTime) > timedelta(hours=5)):
startTime = datetime.now()
print("Reloading script!")
return 0
print("Segment number: {}".format(segment_number))
print(f"Total number of segments: {len(video_segment_list)}")
if segment_number > (len(video_segment_list)):
dash_content = requests.get(dash_url).text
time.sleep(50)
segment_number = segment_number - 5
continue
# Segment might not be in list yet
except:
print("Exception")
dash_content = requests.get(dash_url).text
time.sleep(10)
segment_number = segment_number - 5
continue
print(f"URL: {video_segment_list[segment_number]}")
if segment_number < len(video_segment_list):
while(True):
try:
if(dash_tries == retries):
print("Exceeded {} retries! Exiting...".format(retries))
return -1
r = session.head(video_segment_list[segment_number])
if r.status_code == 200:
break
dash_tries += 1
time.sleep(4)
except:
dash_tries += 1
time.sleep(4)
os.system("aria2c -c --auto-file-renaming=false --max-tries=100 --retry-wait=5 -j 3 -x 3 -s 3 -k 1M \"{}\" -d \"{}\" -o \"{}\"".format(video_segment_list[segment_number], output_directory, f"{segment_number}_{filename_thing}_video.ts"))
while(True):
try:
r = session.head(audio_segment_list[segment_number])
if r.status_code == 200:
break
time.sleep(2)
except:
time.sleep(2)
os.system("aria2c -c --auto-file-renaming=false --max-tries=100 --retry-wait=5 -j 3 -x 3 -s 3 -k 1M \"{}\" -d \"{}\" -o \"{}\"".format(audio_segment_list[segment_number], output_directory, f"{segment_number}_{filename_thing}_audio.ts"))
try:
if pathlib.Path(output_directory / f"{segment_number}_{filename_thing}_video.ts").stat().st_size < 2000 or pathlib.Path(output_directory / f"{segment_number}_{filename_thing}_audio.ts").stat().st_size < 2000:
segment_number -= 4
print("Trying again!")
continue
else:
# It worked!
segment_number += 1
global startTime
print("Time since last reset: {}".format(datetime.now() - startTime))
except:
segment_number -= 3
print("Trying again!")
continue
if segment_number > biggest_segment:
dash_tries = 0
biggest_segment = segment_number
else:
dash_tries += 1
if segment_number >= len(video_segment_list):
print("Tries: {}".format(dash_tries))
# time.sleep(1)
dash_content = requests.get(dash_url).text
video_segment_list.append(get_new_segment(dash_content, int(chosen_quality_video), segment_number))
audio_segment_list.append(get_new_segment(dash_content, int(chosen_quality_audio), segment_number))
if dash_tries == retries:
print("Exceeded {} retries! Exiting...".format(retries))
return -1
if ((datetime.now() - startTime) > timedelta(hours=5)):
startTime = datetime.now()
print("Reloading script!")
return 0
# Yes I know this is ugly, shut up
ret = 0
while(True):
try:
ret = run_script()
if ret == -1:
exit()
except:
if ret == -1:
exit()
ret = 0
time.sleep(10)
pass
try:
ret = run_script()
if ret == -1:
exit()
except:
if ret == -1:
exit()
ret = 0
time.sleep(10)
pass