Store video comments, fix set file button in video edit window
128
CHANGES
@ -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)
|
||||
|
85
README.rst
@ -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
|
||||
|
BIN
icons/small/comment.png
Normal file
After Width: | Height: | Size: 617 B |
BIN
icons/small/favourite.png
Normal file
After Width: | Height: | Size: 685 B |
BIN
icons/small/likes.png
Normal file
After Width: | Height: | Size: 659 B |
Before Width: | Height: | Size: 871 B After Width: | Height: | Size: 871 B |
Before Width: | Height: | Size: 812 B After Width: | Height: | Size: 812 B |
BIN
icons/small/uploader.png
Normal file
After Width: | Height: | Size: 593 B |
@ -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 ""
|
||||
|
@ -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"
|
||||
|
@ -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"
|
||||
|
||||
|
@ -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"
|
||||
|
@ -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"
|
||||
|
@ -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.
|
||||
|
@ -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.
|
||||
|
@ -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.
|
||||
|
@ -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
|
||||
|
@ -1,6 +1,6 @@
|
||||
[Desktop Entry]
|
||||
Name=Tartube
|
||||
Version=2.3.306
|
||||
Version=2.3.321
|
||||
Exec=tartube
|
||||
Icon=tartube
|
||||
Type=Application
|
||||
|
Before Width: | Height: | Size: 51 KiB After Width: | Height: | Size: 54 KiB |
2
setup.py
@ -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',
|
||||
|
2057
tartube/config.py
@ -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
|
||||
|
@ -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
|
||||
|
||||
"""
|
||||
|
||||
|
@ -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',
|
||||
|
@ -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',
|
||||
}
|
||||
|
||||
|
@ -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'),
|
||||
|
142
tartube/media.py
@ -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
|
||||
|
||||
|
@ -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'] = []
|
||||
|
@ -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(
|
||||
|
@ -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,
|
||||
|
@ -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.
|
||||
|
138
tartube/tidy.py
@ -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
|
||||
|
||||
|
||||
|
@ -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)
|
||||
|
||||
"""
|
||||
|
112
tartube/utils.py
@ -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
|
||||
|
||||
"""
|
||||
|
||||
|
230
ytsc/merge.py
@ -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.")
|
||||
|
@ -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
|
||||
|