Stage new submodule locations
This commit is contained in:
2
submodules/ModernZ/.gitattributes
vendored
Normal file
2
submodules/ModernZ/.gitattributes
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
* text=auto
|
||||||
|
*.conf linguist-language=EditorConfig
|
||||||
128
submodules/ModernZ/.github/CODE_OF_CONDUCT.md
vendored
Normal file
128
submodules/ModernZ/.github/CODE_OF_CONDUCT.md
vendored
Normal file
@@ -0,0 +1,128 @@
|
|||||||
|
# Contributor Covenant Code of Conduct
|
||||||
|
|
||||||
|
## Our Pledge
|
||||||
|
|
||||||
|
We as members, contributors, and leaders pledge to make participation in our
|
||||||
|
community a harassment-free experience for everyone, regardless of age, body
|
||||||
|
size, visible or invisible disability, ethnicity, sex characteristics, gender
|
||||||
|
identity and expression, level of experience, education, socio-economic status,
|
||||||
|
nationality, personal appearance, race, religion, or sexual identity
|
||||||
|
and orientation.
|
||||||
|
|
||||||
|
We pledge to act and interact in ways that contribute to an open, welcoming,
|
||||||
|
diverse, inclusive, and healthy community.
|
||||||
|
|
||||||
|
## Our Standards
|
||||||
|
|
||||||
|
Examples of behavior that contributes to a positive environment for our
|
||||||
|
community include:
|
||||||
|
|
||||||
|
* Demonstrating empathy and kindness toward other people
|
||||||
|
* Being respectful of differing opinions, viewpoints, and experiences
|
||||||
|
* Giving and gracefully accepting constructive feedback
|
||||||
|
* Accepting responsibility and apologizing to those affected by our mistakes,
|
||||||
|
and learning from the experience
|
||||||
|
* Focusing on what is best not just for us as individuals, but for the
|
||||||
|
overall community
|
||||||
|
|
||||||
|
Examples of unacceptable behavior include:
|
||||||
|
|
||||||
|
* The use of sexualized language or imagery, and sexual attention or
|
||||||
|
advances of any kind
|
||||||
|
* Trolling, insulting or derogatory comments, and personal or political attacks
|
||||||
|
* Public or private harassment
|
||||||
|
* Publishing others' private information, such as a physical or email
|
||||||
|
address, without their explicit permission
|
||||||
|
* Other conduct which could reasonably be considered inappropriate in a
|
||||||
|
professional setting
|
||||||
|
|
||||||
|
## Enforcement Responsibilities
|
||||||
|
|
||||||
|
Community leaders are responsible for clarifying and enforcing our standards of
|
||||||
|
acceptable behavior and will take appropriate and fair corrective action in
|
||||||
|
response to any behavior that they deem inappropriate, threatening, offensive,
|
||||||
|
or harmful.
|
||||||
|
|
||||||
|
Community leaders have the right and responsibility to remove, edit, or reject
|
||||||
|
comments, commits, code, wiki edits, issues, and other contributions that are
|
||||||
|
not aligned to this Code of Conduct, and will communicate reasons for moderation
|
||||||
|
decisions when appropriate.
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
This Code of Conduct applies within all community spaces, and also applies when
|
||||||
|
an individual is officially representing the community in public spaces.
|
||||||
|
Examples of representing our community include using an official e-mail address,
|
||||||
|
posting via an official social media account, or acting as an appointed
|
||||||
|
representative at an online or offline event.
|
||||||
|
|
||||||
|
## Enforcement
|
||||||
|
|
||||||
|
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||||
|
reported to the community leaders responsible for enforcement at
|
||||||
|
ModernZ Github Issues.
|
||||||
|
All complaints will be reviewed and investigated promptly and fairly.
|
||||||
|
|
||||||
|
All community leaders are obligated to respect the privacy and security of the
|
||||||
|
reporter of any incident.
|
||||||
|
|
||||||
|
## Enforcement Guidelines
|
||||||
|
|
||||||
|
Community leaders will follow these Community Impact Guidelines in determining
|
||||||
|
the consequences for any action they deem in violation of this Code of Conduct:
|
||||||
|
|
||||||
|
### 1. Correction
|
||||||
|
|
||||||
|
**Community Impact**: Use of inappropriate language or other behavior deemed
|
||||||
|
unprofessional or unwelcome in the community.
|
||||||
|
|
||||||
|
**Consequence**: A private, written warning from community leaders, providing
|
||||||
|
clarity around the nature of the violation and an explanation of why the
|
||||||
|
behavior was inappropriate. A public apology may be requested.
|
||||||
|
|
||||||
|
### 2. Warning
|
||||||
|
|
||||||
|
**Community Impact**: A violation through a single incident or series
|
||||||
|
of actions.
|
||||||
|
|
||||||
|
**Consequence**: A warning with consequences for continued behavior. No
|
||||||
|
interaction with the people involved, including unsolicited interaction with
|
||||||
|
those enforcing the Code of Conduct, for a specified period of time. This
|
||||||
|
includes avoiding interactions in community spaces as well as external channels
|
||||||
|
like social media. Violating these terms may lead to a temporary or
|
||||||
|
permanent ban.
|
||||||
|
|
||||||
|
### 3. Temporary Ban
|
||||||
|
|
||||||
|
**Community Impact**: A serious violation of community standards, including
|
||||||
|
sustained inappropriate behavior.
|
||||||
|
|
||||||
|
**Consequence**: A temporary ban from any sort of interaction or public
|
||||||
|
communication with the community for a specified period of time. No public or
|
||||||
|
private interaction with the people involved, including unsolicited interaction
|
||||||
|
with those enforcing the Code of Conduct, is allowed during this period.
|
||||||
|
Violating these terms may lead to a permanent ban.
|
||||||
|
|
||||||
|
### 4. Permanent Ban
|
||||||
|
|
||||||
|
**Community Impact**: Demonstrating a pattern of violation of community
|
||||||
|
standards, including sustained inappropriate behavior, harassment of an
|
||||||
|
individual, or aggression toward or disparagement of classes of individuals.
|
||||||
|
|
||||||
|
**Consequence**: A permanent ban from any sort of public interaction within
|
||||||
|
the community.
|
||||||
|
|
||||||
|
## Attribution
|
||||||
|
|
||||||
|
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
|
||||||
|
version 2.0, available at
|
||||||
|
https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
|
||||||
|
|
||||||
|
Community Impact Guidelines were inspired by [Mozilla's code of conduct
|
||||||
|
enforcement ladder](https://github.com/mozilla/diversity).
|
||||||
|
|
||||||
|
[homepage]: https://www.contributor-covenant.org
|
||||||
|
|
||||||
|
For answers to common questions about this code of conduct, see the FAQ at
|
||||||
|
https://www.contributor-covenant.org/faq. Translations are available at
|
||||||
|
https://www.contributor-covenant.org/translations.
|
||||||
99
submodules/ModernZ/.github/CONTRIBUTING.md
vendored
Normal file
99
submodules/ModernZ/.github/CONTRIBUTING.md
vendored
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
How to contribute
|
||||||
|
=================
|
||||||
|
|
||||||
|
General
|
||||||
|
-------
|
||||||
|
|
||||||
|
All contributions are generally welcomed. We always appreciate the effort and time taken by others to improve or fix ModernZ.
|
||||||
|
|
||||||
|
If you're hesitant or not sure about a specific contribution, you can always open an issue to ask a question or discuss something in detail to see if your contribution goes with ModernZ plans or not.
|
||||||
|
|
||||||
|
Main project goals
|
||||||
|
------------------
|
||||||
|
|
||||||
|
- Revive and maintain the modern osc origin, as it is either abandoned or partially maintained by adding new code without fixing the old.
|
||||||
|
- Apply common features that are missing and most users would want (ie: customization, button functions, lists and so on)
|
||||||
|
|
||||||
|
ModernZ is trying to be in a middle area between stock OSC and a project like uosc. One is too simple, the other is too complicated.
|
||||||
|
|
||||||
|
One of the first things that happened in ModernZ was to re-base on stock osc and integrate features that mpv already has, which eliminated a ton of bloat from the code.
|
||||||
|
|
||||||
|
In essence, we want to be better than stock OSC, but we don't want to do everything. That is why for example we integrated locale as extras, and the same thing was done for the pause indicator.
|
||||||
|
|
||||||
|
If it's useful, but not necessary to the OSC, it should be in extras or an independent script. Modular methodology.
|
||||||
|
|
||||||
|
Sending patches
|
||||||
|
---------------
|
||||||
|
|
||||||
|
- Make a GitHub pull request.
|
||||||
|
- Plain diffs posted as pastebins are not acceptable. They only cause extra work for everyone, because they lack
|
||||||
|
commit message and authorship information.
|
||||||
|
- Be sure to test your changes. If you didn't, please say so in the commit
|
||||||
|
message and the pull request text.
|
||||||
|
|
||||||
|
Write good commit messages
|
||||||
|
--------------------------
|
||||||
|
|
||||||
|
- Write informative commit messages. Use present tense to describe the
|
||||||
|
situation with the patch applied, and past tense for the situation before
|
||||||
|
the change.
|
||||||
|
- The subject line (the first line in a commit message) must contain an
|
||||||
|
identifying prefix, followed by a short description what
|
||||||
|
impact this commit has on ModernZ osc.
|
||||||
|
|
||||||
|
Split changes into multiple commits
|
||||||
|
-----------------------------------
|
||||||
|
|
||||||
|
- Follow git good practices, and split independent changes into several commits.
|
||||||
|
It's usually OK to put them into a single pull request.
|
||||||
|
- Try to separate cosmetic and functional changes. It's ok to make a few
|
||||||
|
additional cosmetic changes in the same file you're working on. However, don't do
|
||||||
|
something like reformatting a whole file, and hiding an actual functional
|
||||||
|
change in the same commit.
|
||||||
|
- Splitting changes does _not_ mean that you should make them as fine-grained
|
||||||
|
as possible. Commits should form logical steps in development. The way you
|
||||||
|
split changes is important for code review and analyzing bugs.
|
||||||
|
- We prefer to have pull requests in a merge
|
||||||
|
ready state.
|
||||||
|
|
||||||
|
Touching user-visible elements may require updating the docs
|
||||||
|
-------------------------------------------------------------
|
||||||
|
|
||||||
|
- Most user-visible elements are normally documented in docs/. If your commit
|
||||||
|
touches documented behavior, list of options, etc., you need to adjust the
|
||||||
|
documentation.
|
||||||
|
- These changes usually go into the same commit that changes the code.
|
||||||
|
|
||||||
|
Code of Conduct
|
||||||
|
---------------
|
||||||
|
|
||||||
|
Please note that this project is released with a Contributor Code of Conduct.
|
||||||
|
By participating in this project you agree to abide by its terms.
|
||||||
|
The Contributor Code of Conduct can be found here:
|
||||||
|
https://www.contributor-covenant.org/version/2/0/code_of_conduct/
|
||||||
|
|
||||||
|
Copyright of contributions
|
||||||
|
--------------------------
|
||||||
|
|
||||||
|
- The copyright belongs to contributors. The project is a collaborative work. By
|
||||||
|
sending your changes, you agree to license your contributions according to the
|
||||||
|
requirements of this project.
|
||||||
|
- All new code must be LGPLv2.1+ licensed, or come with the implicit agreement
|
||||||
|
that it will be relicensed to LGPLv2.1+ later.
|
||||||
|
- 100% compatible licenses are allowed too.
|
||||||
|
- Changes in files with more liberal licenses (such as BSD, MIT, or ISC) are
|
||||||
|
assumed to be dual-licensed under LGPLv2.1+ and the license indicated in the
|
||||||
|
file header.
|
||||||
|
- You must be either the exclusive author of the patch, or acknowledge all
|
||||||
|
authors involved in the commit message. If you take 3rd party code, authorship
|
||||||
|
and copyright must be properly acknowledged. If you're making changes on
|
||||||
|
behalf of your employer, and the employer owns the copyright, you must mention
|
||||||
|
this. If the license of the code is not LGPLv2.1+, you must mention this.
|
||||||
|
- These license statements are legally binding.
|
||||||
|
- Don't use fake names (something that looks like an actual name, and may be
|
||||||
|
someone else's name, but is not your legal name). Using a pseudonym is
|
||||||
|
allowed if it can be used to identify or contact you, even if whatever
|
||||||
|
account you used to submit the patch dies.
|
||||||
|
- Do not add your name to the license header. This convention is not used by
|
||||||
|
this project, and neither copyright law nor any of the used licenses require
|
||||||
|
it.
|
||||||
15
submodules/ModernZ/.github/ISSUE_TEMPLATE/1_feature_request.yml
vendored
Normal file
15
submodules/ModernZ/.github/ISSUE_TEMPLATE/1_feature_request.yml
vendored
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
name: "Request a new Feature"
|
||||||
|
description: "Create a request for a new feature"
|
||||||
|
labels: ["feature request"]
|
||||||
|
body:
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: "Expected behavior of the wanted feature"
|
||||||
|
description: >
|
||||||
|
Before requesting a new feature, make sure it hasn't been
|
||||||
|
[requested yet](https://github.com/Samillion/ModernZ/labels/feature%20request).
|
||||||
|
|
||||||
|
If you are not sure about this, please read
|
||||||
|
[FAQ and guidelines](https://github.com/Samillion/ModernZ/blob/main/docs/FAQ.md) first.
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
74
submodules/ModernZ/.github/ISSUE_TEMPLATE/2_bug_report.yml
vendored
Normal file
74
submodules/ModernZ/.github/ISSUE_TEMPLATE/2_bug_report.yml
vendored
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
name: "Report an issue or a bug"
|
||||||
|
description: "Create a report for an issue or a bug"
|
||||||
|
labels: ["bug"]
|
||||||
|
body:
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: "mpv Information"
|
||||||
|
placeholder: |
|
||||||
|
mpv v0.39.0-300-g42ff6f92 Copyright © 2000-2024 mpv/MPlayer/mplayer2 projects
|
||||||
|
built on Nov 3 2024 00:06:03
|
||||||
|
libplacebo version: v7.349.0 (v7.349.0-19-g118d810-dirty)
|
||||||
|
FFmpeg version: N-117698-gc325f9c61
|
||||||
|
FFmpeg library versions:
|
||||||
|
libavcodec 61.23.100
|
||||||
|
libavdevice 61.4.100
|
||||||
|
libavfilter 10.6.101
|
||||||
|
libavformat 61.9.100
|
||||||
|
libavutil 59.46.100
|
||||||
|
libswscale 8.9.101
|
||||||
|
description: |
|
||||||
|
Provide the output of `mpv --version`.
|
||||||
|
render: bash
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: "Reproduction Steps"
|
||||||
|
description: >
|
||||||
|
Try to reproduce your issue with `--no-config` and mpv's stock osc first. If it is reproducible
|
||||||
|
even then, try to find out which option or script causes your issue.
|
||||||
|
|
||||||
|
|
||||||
|
Describe the reproduction steps as precise as possible. It's very likely that
|
||||||
|
the bug you experience wasn't reproduced by the developer because the workflow
|
||||||
|
differs from your own.
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: "Expected Behavior"
|
||||||
|
placeholder: "What were you expecting?"
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: "Actual Behavior"
|
||||||
|
placeholder: "What happened instead?"
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: "Log File"
|
||||||
|
placeholder: "Drag and drop log file here (Don't paste content directly)"
|
||||||
|
description: >
|
||||||
|
Make a log file with `--log-file=output.txt` and
|
||||||
|
attach it to the issue. [[details](https://mpv.io/manual/master/#options-log-file)]
|
||||||
|
|
||||||
|
|
||||||
|
If you have trouble producing a log file, you can alternatively use `-v -v`,
|
||||||
|
save the terminal output to a file, and attach it to the issue.
|
||||||
|
|
||||||
|
|
||||||
|
Disclaimer: The log file may contain identifiable information, such as your username.
|
||||||
|
Review the log file before sharing and redact any sensitive data if necessary.
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
- type: checkboxes
|
||||||
|
attributes:
|
||||||
|
label: "I carefully read all instructions and confirm that I did the following:"
|
||||||
|
options:
|
||||||
|
- label: "I tested with the latest mpv version to validate that the issue is not already fixed."
|
||||||
|
required: true
|
||||||
|
- label: "I provided all required information."
|
||||||
|
required: true
|
||||||
8
submodules/ModernZ/.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
8
submodules/ModernZ/.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
blank_issues_enabled: false
|
||||||
|
contact_links:
|
||||||
|
- name: FAQ and guidelines (Important)
|
||||||
|
url: https://github.com/Samillion/ModernZ/blob/main/docs/FAQ.md
|
||||||
|
about: Frequent questions are answered there, also includes useful guidelines
|
||||||
|
- name: Discussions and Questions
|
||||||
|
url: https://github.com/Samillion/ModernZ/discussions
|
||||||
|
about: A place to discuss or ask a general question regarding ModernZ osc
|
||||||
7
submodules/ModernZ/.github/PULL_REQUEST_TEMPLATE
vendored
Normal file
7
submodules/ModernZ/.github/PULL_REQUEST_TEMPLATE
vendored
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
Read this before you submit a pull request please:
|
||||||
|
https://github.com/Samillion/ModernZ/blob/main/.github/CONTRIBUTING.md
|
||||||
|
|
||||||
|
Reading this link and following the rules will get your pull request reviewed
|
||||||
|
and merged faster.
|
||||||
|
|
||||||
|
Delete this message after reading the link
|
||||||
504
submodules/ModernZ/LICENSE
Normal file
504
submodules/ModernZ/LICENSE
Normal file
@@ -0,0 +1,504 @@
|
|||||||
|
GNU LESSER GENERAL PUBLIC LICENSE
|
||||||
|
Version 2.1, February 1999
|
||||||
|
|
||||||
|
Copyright (C) 1991, 1999 Free Software Foundation, Inc.
|
||||||
|
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||||
|
Everyone is permitted to copy and distribute verbatim copies
|
||||||
|
of this license document, but changing it is not allowed.
|
||||||
|
|
||||||
|
[This is the first released version of the Lesser GPL. It also counts
|
||||||
|
as the successor of the GNU Library Public License, version 2, hence
|
||||||
|
the version number 2.1.]
|
||||||
|
|
||||||
|
Preamble
|
||||||
|
|
||||||
|
The licenses for most software are designed to take away your
|
||||||
|
freedom to share and change it. By contrast, the GNU General Public
|
||||||
|
Licenses are intended to guarantee your freedom to share and change
|
||||||
|
free software--to make sure the software is free for all its users.
|
||||||
|
|
||||||
|
This license, the Lesser General Public License, applies to some
|
||||||
|
specially designated software packages--typically libraries--of the
|
||||||
|
Free Software Foundation and other authors who decide to use it. You
|
||||||
|
can use it too, but we suggest you first think carefully about whether
|
||||||
|
this license or the ordinary General Public License is the better
|
||||||
|
strategy to use in any particular case, based on the explanations below.
|
||||||
|
|
||||||
|
When we speak of free software, we are referring to freedom of use,
|
||||||
|
not price. Our General Public Licenses are designed to make sure that
|
||||||
|
you have the freedom to distribute copies of free software (and charge
|
||||||
|
for this service if you wish); that you receive source code or can get
|
||||||
|
it if you want it; that you can change the software and use pieces of
|
||||||
|
it in new free programs; and that you are informed that you can do
|
||||||
|
these things.
|
||||||
|
|
||||||
|
To protect your rights, we need to make restrictions that forbid
|
||||||
|
distributors to deny you these rights or to ask you to surrender these
|
||||||
|
rights. These restrictions translate to certain responsibilities for
|
||||||
|
you if you distribute copies of the library or if you modify it.
|
||||||
|
|
||||||
|
For example, if you distribute copies of the library, whether gratis
|
||||||
|
or for a fee, you must give the recipients all the rights that we gave
|
||||||
|
you. You must make sure that they, too, receive or can get the source
|
||||||
|
code. If you link other code with the library, you must provide
|
||||||
|
complete object files to the recipients, so that they can relink them
|
||||||
|
with the library after making changes to the library and recompiling
|
||||||
|
it. And you must show them these terms so they know their rights.
|
||||||
|
|
||||||
|
We protect your rights with a two-step method: (1) we copyright the
|
||||||
|
library, and (2) we offer you this license, which gives you legal
|
||||||
|
permission to copy, distribute and/or modify the library.
|
||||||
|
|
||||||
|
To protect each distributor, we want to make it very clear that
|
||||||
|
there is no warranty for the free library. Also, if the library is
|
||||||
|
modified by someone else and passed on, the recipients should know
|
||||||
|
that what they have is not the original version, so that the original
|
||||||
|
author's reputation will not be affected by problems that might be
|
||||||
|
introduced by others.
|
||||||
|
|
||||||
|
Finally, software patents pose a constant threat to the existence of
|
||||||
|
any free program. We wish to make sure that a company cannot
|
||||||
|
effectively restrict the users of a free program by obtaining a
|
||||||
|
restrictive license from a patent holder. Therefore, we insist that
|
||||||
|
any patent license obtained for a version of the library must be
|
||||||
|
consistent with the full freedom of use specified in this license.
|
||||||
|
|
||||||
|
Most GNU software, including some libraries, is covered by the
|
||||||
|
ordinary GNU General Public License. This license, the GNU Lesser
|
||||||
|
General Public License, applies to certain designated libraries, and
|
||||||
|
is quite different from the ordinary General Public License. We use
|
||||||
|
this license for certain libraries in order to permit linking those
|
||||||
|
libraries into non-free programs.
|
||||||
|
|
||||||
|
When a program is linked with a library, whether statically or using
|
||||||
|
a shared library, the combination of the two is legally speaking a
|
||||||
|
combined work, a derivative of the original library. The ordinary
|
||||||
|
General Public License therefore permits such linking only if the
|
||||||
|
entire combination fits its criteria of freedom. The Lesser General
|
||||||
|
Public License permits more lax criteria for linking other code with
|
||||||
|
the library.
|
||||||
|
|
||||||
|
We call this license the "Lesser" General Public License because it
|
||||||
|
does Less to protect the user's freedom than the ordinary General
|
||||||
|
Public License. It also provides other free software developers Less
|
||||||
|
of an advantage over competing non-free programs. These disadvantages
|
||||||
|
are the reason we use the ordinary General Public License for many
|
||||||
|
libraries. However, the Lesser license provides advantages in certain
|
||||||
|
special circumstances.
|
||||||
|
|
||||||
|
For example, on rare occasions, there may be a special need to
|
||||||
|
encourage the widest possible use of a certain library, so that it becomes
|
||||||
|
a de-facto standard. To achieve this, non-free programs must be
|
||||||
|
allowed to use the library. A more frequent case is that a free
|
||||||
|
library does the same job as widely used non-free libraries. In this
|
||||||
|
case, there is little to gain by limiting the free library to free
|
||||||
|
software only, so we use the Lesser General Public License.
|
||||||
|
|
||||||
|
In other cases, permission to use a particular library in non-free
|
||||||
|
programs enables a greater number of people to use a large body of
|
||||||
|
free software. For example, permission to use the GNU C Library in
|
||||||
|
non-free programs enables many more people to use the whole GNU
|
||||||
|
operating system, as well as its variant, the GNU/Linux operating
|
||||||
|
system.
|
||||||
|
|
||||||
|
Although the Lesser General Public License is Less protective of the
|
||||||
|
users' freedom, it does ensure that the user of a program that is
|
||||||
|
linked with the Library has the freedom and the wherewithal to run
|
||||||
|
that program using a modified version of the Library.
|
||||||
|
|
||||||
|
The precise terms and conditions for copying, distribution and
|
||||||
|
modification follow. Pay close attention to the difference between a
|
||||||
|
"work based on the library" and a "work that uses the library". The
|
||||||
|
former contains code derived from the library, whereas the latter must
|
||||||
|
be combined with the library in order to run.
|
||||||
|
|
||||||
|
GNU LESSER GENERAL PUBLIC LICENSE
|
||||||
|
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
|
||||||
|
|
||||||
|
0. This License Agreement applies to any software library or other
|
||||||
|
program which contains a notice placed by the copyright holder or
|
||||||
|
other authorized party saying it may be distributed under the terms of
|
||||||
|
this Lesser General Public License (also called "this License").
|
||||||
|
Each licensee is addressed as "you".
|
||||||
|
|
||||||
|
A "library" means a collection of software functions and/or data
|
||||||
|
prepared so as to be conveniently linked with application programs
|
||||||
|
(which use some of those functions and data) to form executables.
|
||||||
|
|
||||||
|
The "Library", below, refers to any such software library or work
|
||||||
|
which has been distributed under these terms. A "work based on the
|
||||||
|
Library" means either the Library or any derivative work under
|
||||||
|
copyright law: that is to say, a work containing the Library or a
|
||||||
|
portion of it, either verbatim or with modifications and/or translated
|
||||||
|
straightforwardly into another language. (Hereinafter, translation is
|
||||||
|
included without limitation in the term "modification".)
|
||||||
|
|
||||||
|
"Source code" for a work means the preferred form of the work for
|
||||||
|
making modifications to it. For a library, complete source code means
|
||||||
|
all the source code for all modules it contains, plus any associated
|
||||||
|
interface definition files, plus the scripts used to control compilation
|
||||||
|
and installation of the library.
|
||||||
|
|
||||||
|
Activities other than copying, distribution and modification are not
|
||||||
|
covered by this License; they are outside its scope. The act of
|
||||||
|
running a program using the Library is not restricted, and output from
|
||||||
|
such a program is covered only if its contents constitute a work based
|
||||||
|
on the Library (independent of the use of the Library in a tool for
|
||||||
|
writing it). Whether that is true depends on what the Library does
|
||||||
|
and what the program that uses the Library does.
|
||||||
|
|
||||||
|
1. You may copy and distribute verbatim copies of the Library's
|
||||||
|
complete source code as you receive it, in any medium, provided that
|
||||||
|
you conspicuously and appropriately publish on each copy an
|
||||||
|
appropriate copyright notice and disclaimer of warranty; keep intact
|
||||||
|
all the notices that refer to this License and to the absence of any
|
||||||
|
warranty; and distribute a copy of this License along with the
|
||||||
|
Library.
|
||||||
|
|
||||||
|
You may charge a fee for the physical act of transferring a copy,
|
||||||
|
and you may at your option offer warranty protection in exchange for a
|
||||||
|
fee.
|
||||||
|
|
||||||
|
2. You may modify your copy or copies of the Library or any portion
|
||||||
|
of it, thus forming a work based on the Library, and copy and
|
||||||
|
distribute such modifications or work under the terms of Section 1
|
||||||
|
above, provided that you also meet all of these conditions:
|
||||||
|
|
||||||
|
a) The modified work must itself be a software library.
|
||||||
|
|
||||||
|
b) You must cause the files modified to carry prominent notices
|
||||||
|
stating that you changed the files and the date of any change.
|
||||||
|
|
||||||
|
c) You must cause the whole of the work to be licensed at no
|
||||||
|
charge to all third parties under the terms of this License.
|
||||||
|
|
||||||
|
d) If a facility in the modified Library refers to a function or a
|
||||||
|
table of data to be supplied by an application program that uses
|
||||||
|
the facility, other than as an argument passed when the facility
|
||||||
|
is invoked, then you must make a good faith effort to ensure that,
|
||||||
|
in the event an application does not supply such function or
|
||||||
|
table, the facility still operates, and performs whatever part of
|
||||||
|
its purpose remains meaningful.
|
||||||
|
|
||||||
|
(For example, a function in a library to compute square roots has
|
||||||
|
a purpose that is entirely well-defined independent of the
|
||||||
|
application. Therefore, Subsection 2d requires that any
|
||||||
|
application-supplied function or table used by this function must
|
||||||
|
be optional: if the application does not supply it, the square
|
||||||
|
root function must still compute square roots.)
|
||||||
|
|
||||||
|
These requirements apply to the modified work as a whole. If
|
||||||
|
identifiable sections of that work are not derived from the Library,
|
||||||
|
and can be reasonably considered independent and separate works in
|
||||||
|
themselves, then this License, and its terms, do not apply to those
|
||||||
|
sections when you distribute them as separate works. But when you
|
||||||
|
distribute the same sections as part of a whole which is a work based
|
||||||
|
on the Library, the distribution of the whole must be on the terms of
|
||||||
|
this License, whose permissions for other licensees extend to the
|
||||||
|
entire whole, and thus to each and every part regardless of who wrote
|
||||||
|
it.
|
||||||
|
|
||||||
|
Thus, it is not the intent of this section to claim rights or contest
|
||||||
|
your rights to work written entirely by you; rather, the intent is to
|
||||||
|
exercise the right to control the distribution of derivative or
|
||||||
|
collective works based on the Library.
|
||||||
|
|
||||||
|
In addition, mere aggregation of another work not based on the Library
|
||||||
|
with the Library (or with a work based on the Library) on a volume of
|
||||||
|
a storage or distribution medium does not bring the other work under
|
||||||
|
the scope of this License.
|
||||||
|
|
||||||
|
3. You may opt to apply the terms of the ordinary GNU General Public
|
||||||
|
License instead of this License to a given copy of the Library. To do
|
||||||
|
this, you must alter all the notices that refer to this License, so
|
||||||
|
that they refer to the ordinary GNU General Public License, version 2,
|
||||||
|
instead of to this License. (If a newer version than version 2 of the
|
||||||
|
ordinary GNU General Public License has appeared, then you can specify
|
||||||
|
that version instead if you wish.) Do not make any other change in
|
||||||
|
these notices.
|
||||||
|
|
||||||
|
Once this change is made in a given copy, it is irreversible for
|
||||||
|
that copy, so the ordinary GNU General Public License applies to all
|
||||||
|
subsequent copies and derivative works made from that copy.
|
||||||
|
|
||||||
|
This option is useful when you wish to copy part of the code of
|
||||||
|
the Library into a program that is not a library.
|
||||||
|
|
||||||
|
4. You may copy and distribute the Library (or a portion or
|
||||||
|
derivative of it, under Section 2) in object code or executable form
|
||||||
|
under the terms of Sections 1 and 2 above provided that you accompany
|
||||||
|
it with the complete corresponding machine-readable source code, which
|
||||||
|
must be distributed under the terms of Sections 1 and 2 above on a
|
||||||
|
medium customarily used for software interchange.
|
||||||
|
|
||||||
|
If distribution of object code is made by offering access to copy
|
||||||
|
from a designated place, then offering equivalent access to copy the
|
||||||
|
source code from the same place satisfies the requirement to
|
||||||
|
distribute the source code, even though third parties are not
|
||||||
|
compelled to copy the source along with the object code.
|
||||||
|
|
||||||
|
5. A program that contains no derivative of any portion of the
|
||||||
|
Library, but is designed to work with the Library by being compiled or
|
||||||
|
linked with it, is called a "work that uses the Library". Such a
|
||||||
|
work, in isolation, is not a derivative work of the Library, and
|
||||||
|
therefore falls outside the scope of this License.
|
||||||
|
|
||||||
|
However, linking a "work that uses the Library" with the Library
|
||||||
|
creates an executable that is a derivative of the Library (because it
|
||||||
|
contains portions of the Library), rather than a "work that uses the
|
||||||
|
library". The executable is therefore covered by this License.
|
||||||
|
Section 6 states terms for distribution of such executables.
|
||||||
|
|
||||||
|
When a "work that uses the Library" uses material from a header file
|
||||||
|
that is part of the Library, the object code for the work may be a
|
||||||
|
derivative work of the Library even though the source code is not.
|
||||||
|
Whether this is true is especially significant if the work can be
|
||||||
|
linked without the Library, or if the work is itself a library. The
|
||||||
|
threshold for this to be true is not precisely defined by law.
|
||||||
|
|
||||||
|
If such an object file uses only numerical parameters, data
|
||||||
|
structure layouts and accessors, and small macros and small inline
|
||||||
|
functions (ten lines or less in length), then the use of the object
|
||||||
|
file is unrestricted, regardless of whether it is legally a derivative
|
||||||
|
work. (Executables containing this object code plus portions of the
|
||||||
|
Library will still fall under Section 6.)
|
||||||
|
|
||||||
|
Otherwise, if the work is a derivative of the Library, you may
|
||||||
|
distribute the object code for the work under the terms of Section 6.
|
||||||
|
Any executables containing that work also fall under Section 6,
|
||||||
|
whether or not they are linked directly with the Library itself.
|
||||||
|
|
||||||
|
6. As an exception to the Sections above, you may also combine or
|
||||||
|
link a "work that uses the Library" with the Library to produce a
|
||||||
|
work containing portions of the Library, and distribute that work
|
||||||
|
under terms of your choice, provided that the terms permit
|
||||||
|
modification of the work for the customer's own use and reverse
|
||||||
|
engineering for debugging such modifications.
|
||||||
|
|
||||||
|
You must give prominent notice with each copy of the work that the
|
||||||
|
Library is used in it and that the Library and its use are covered by
|
||||||
|
this License. You must supply a copy of this License. If the work
|
||||||
|
during execution displays copyright notices, you must include the
|
||||||
|
copyright notice for the Library among them, as well as a reference
|
||||||
|
directing the user to the copy of this License. Also, you must do one
|
||||||
|
of these things:
|
||||||
|
|
||||||
|
a) Accompany the work with the complete corresponding
|
||||||
|
machine-readable source code for the Library including whatever
|
||||||
|
changes were used in the work (which must be distributed under
|
||||||
|
Sections 1 and 2 above); and, if the work is an executable linked
|
||||||
|
with the Library, with the complete machine-readable "work that
|
||||||
|
uses the Library", as object code and/or source code, so that the
|
||||||
|
user can modify the Library and then relink to produce a modified
|
||||||
|
executable containing the modified Library. (It is understood
|
||||||
|
that the user who changes the contents of definitions files in the
|
||||||
|
Library will not necessarily be able to recompile the application
|
||||||
|
to use the modified definitions.)
|
||||||
|
|
||||||
|
b) Use a suitable shared library mechanism for linking with the
|
||||||
|
Library. A suitable mechanism is one that (1) uses at run time a
|
||||||
|
copy of the library already present on the user's computer system,
|
||||||
|
rather than copying library functions into the executable, and (2)
|
||||||
|
will operate properly with a modified version of the library, if
|
||||||
|
the user installs one, as long as the modified version is
|
||||||
|
interface-compatible with the version that the work was made with.
|
||||||
|
|
||||||
|
c) Accompany the work with a written offer, valid for at
|
||||||
|
least three years, to give the same user the materials
|
||||||
|
specified in Subsection 6a, above, for a charge no more
|
||||||
|
than the cost of performing this distribution.
|
||||||
|
|
||||||
|
d) If distribution of the work is made by offering access to copy
|
||||||
|
from a designated place, offer equivalent access to copy the above
|
||||||
|
specified materials from the same place.
|
||||||
|
|
||||||
|
e) Verify that the user has already received a copy of these
|
||||||
|
materials or that you have already sent this user a copy.
|
||||||
|
|
||||||
|
For an executable, the required form of the "work that uses the
|
||||||
|
Library" must include any data and utility programs needed for
|
||||||
|
reproducing the executable from it. However, as a special exception,
|
||||||
|
the materials to be distributed need not include anything that is
|
||||||
|
normally distributed (in either source or binary form) with the major
|
||||||
|
components (compiler, kernel, and so on) of the operating system on
|
||||||
|
which the executable runs, unless that component itself accompanies
|
||||||
|
the executable.
|
||||||
|
|
||||||
|
It may happen that this requirement contradicts the license
|
||||||
|
restrictions of other proprietary libraries that do not normally
|
||||||
|
accompany the operating system. Such a contradiction means you cannot
|
||||||
|
use both them and the Library together in an executable that you
|
||||||
|
distribute.
|
||||||
|
|
||||||
|
7. You may place library facilities that are a work based on the
|
||||||
|
Library side-by-side in a single library together with other library
|
||||||
|
facilities not covered by this License, and distribute such a combined
|
||||||
|
library, provided that the separate distribution of the work based on
|
||||||
|
the Library and of the other library facilities is otherwise
|
||||||
|
permitted, and provided that you do these two things:
|
||||||
|
|
||||||
|
a) Accompany the combined library with a copy of the same work
|
||||||
|
based on the Library, uncombined with any other library
|
||||||
|
facilities. This must be distributed under the terms of the
|
||||||
|
Sections above.
|
||||||
|
|
||||||
|
b) Give prominent notice with the combined library of the fact
|
||||||
|
that part of it is a work based on the Library, and explaining
|
||||||
|
where to find the accompanying uncombined form of the same work.
|
||||||
|
|
||||||
|
8. You may not copy, modify, sublicense, link with, or distribute
|
||||||
|
the Library except as expressly provided under this License. Any
|
||||||
|
attempt otherwise to copy, modify, sublicense, link with, or
|
||||||
|
distribute the Library is void, and will automatically terminate your
|
||||||
|
rights under this License. However, parties who have received copies,
|
||||||
|
or rights, from you under this License will not have their licenses
|
||||||
|
terminated so long as such parties remain in full compliance.
|
||||||
|
|
||||||
|
9. You are not required to accept this License, since you have not
|
||||||
|
signed it. However, nothing else grants you permission to modify or
|
||||||
|
distribute the Library or its derivative works. These actions are
|
||||||
|
prohibited by law if you do not accept this License. Therefore, by
|
||||||
|
modifying or distributing the Library (or any work based on the
|
||||||
|
Library), you indicate your acceptance of this License to do so, and
|
||||||
|
all its terms and conditions for copying, distributing or modifying
|
||||||
|
the Library or works based on it.
|
||||||
|
|
||||||
|
10. Each time you redistribute the Library (or any work based on the
|
||||||
|
Library), the recipient automatically receives a license from the
|
||||||
|
original licensor to copy, distribute, link with or modify the Library
|
||||||
|
subject to these terms and conditions. You may not impose any further
|
||||||
|
restrictions on the recipients' exercise of the rights granted herein.
|
||||||
|
You are not responsible for enforcing compliance by third parties with
|
||||||
|
this License.
|
||||||
|
|
||||||
|
11. If, as a consequence of a court judgment or allegation of patent
|
||||||
|
infringement or for any other reason (not limited to patent issues),
|
||||||
|
conditions are imposed on you (whether by court order, agreement or
|
||||||
|
otherwise) that contradict the conditions of this License, they do not
|
||||||
|
excuse you from the conditions of this License. If you cannot
|
||||||
|
distribute so as to satisfy simultaneously your obligations under this
|
||||||
|
License and any other pertinent obligations, then as a consequence you
|
||||||
|
may not distribute the Library at all. For example, if a patent
|
||||||
|
license would not permit royalty-free redistribution of the Library by
|
||||||
|
all those who receive copies directly or indirectly through you, then
|
||||||
|
the only way you could satisfy both it and this License would be to
|
||||||
|
refrain entirely from distribution of the Library.
|
||||||
|
|
||||||
|
If any portion of this section is held invalid or unenforceable under any
|
||||||
|
particular circumstance, the balance of the section is intended to apply,
|
||||||
|
and the section as a whole is intended to apply in other circumstances.
|
||||||
|
|
||||||
|
It is not the purpose of this section to induce you to infringe any
|
||||||
|
patents or other property right claims or to contest validity of any
|
||||||
|
such claims; this section has the sole purpose of protecting the
|
||||||
|
integrity of the free software distribution system which is
|
||||||
|
implemented by public license practices. Many people have made
|
||||||
|
generous contributions to the wide range of software distributed
|
||||||
|
through that system in reliance on consistent application of that
|
||||||
|
system; it is up to the author/donor to decide if he or she is willing
|
||||||
|
to distribute software through any other system and a licensee cannot
|
||||||
|
impose that choice.
|
||||||
|
|
||||||
|
This section is intended to make thoroughly clear what is believed to
|
||||||
|
be a consequence of the rest of this License.
|
||||||
|
|
||||||
|
12. If the distribution and/or use of the Library is restricted in
|
||||||
|
certain countries either by patents or by copyrighted interfaces, the
|
||||||
|
original copyright holder who places the Library under this License may add
|
||||||
|
an explicit geographical distribution limitation excluding those countries,
|
||||||
|
so that distribution is permitted only in or among countries not thus
|
||||||
|
excluded. In such case, this License incorporates the limitation as if
|
||||||
|
written in the body of this License.
|
||||||
|
|
||||||
|
13. The Free Software Foundation may publish revised and/or new
|
||||||
|
versions of the Lesser General Public License from time to time.
|
||||||
|
Such new versions will be similar in spirit to the present version,
|
||||||
|
but may differ in detail to address new problems or concerns.
|
||||||
|
|
||||||
|
Each version is given a distinguishing version number. If the Library
|
||||||
|
specifies a version number of this License which applies to it and
|
||||||
|
"any later version", you have the option of following the terms and
|
||||||
|
conditions either of that version or of any later version published by
|
||||||
|
the Free Software Foundation. If the Library does not specify a
|
||||||
|
license version number, you may choose any version ever published by
|
||||||
|
the Free Software Foundation.
|
||||||
|
|
||||||
|
14. If you wish to incorporate parts of the Library into other free
|
||||||
|
programs whose distribution conditions are incompatible with these,
|
||||||
|
write to the author to ask for permission. For software which is
|
||||||
|
copyrighted by the Free Software Foundation, write to the Free
|
||||||
|
Software Foundation; we sometimes make exceptions for this. Our
|
||||||
|
decision will be guided by the two goals of preserving the free status
|
||||||
|
of all derivatives of our free software and of promoting the sharing
|
||||||
|
and reuse of software generally.
|
||||||
|
|
||||||
|
NO WARRANTY
|
||||||
|
|
||||||
|
15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO
|
||||||
|
WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW.
|
||||||
|
EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR
|
||||||
|
OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY
|
||||||
|
KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE
|
||||||
|
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||||
|
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE
|
||||||
|
LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME
|
||||||
|
THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
||||||
|
|
||||||
|
16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN
|
||||||
|
WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY
|
||||||
|
AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU
|
||||||
|
FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR
|
||||||
|
CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE
|
||||||
|
LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING
|
||||||
|
RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A
|
||||||
|
FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF
|
||||||
|
SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH
|
||||||
|
DAMAGES.
|
||||||
|
|
||||||
|
END OF TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
How to Apply These Terms to Your New Libraries
|
||||||
|
|
||||||
|
If you develop a new library, and you want it to be of the greatest
|
||||||
|
possible use to the public, we recommend making it free software that
|
||||||
|
everyone can redistribute and change. You can do so by permitting
|
||||||
|
redistribution under these terms (or, alternatively, under the terms of the
|
||||||
|
ordinary General Public License).
|
||||||
|
|
||||||
|
To apply these terms, attach the following notices to the library. It is
|
||||||
|
safest to attach them to the start of each source file to most effectively
|
||||||
|
convey the exclusion of warranty; and each file should have at least the
|
||||||
|
"copyright" line and a pointer to where the full notice is found.
|
||||||
|
|
||||||
|
<one line to give the library's name and a brief idea of what it does.>
|
||||||
|
Copyright (C) <year> <name of author>
|
||||||
|
|
||||||
|
This library is free software; you can redistribute it and/or
|
||||||
|
modify it under the terms of the GNU Lesser General Public
|
||||||
|
License as published by the Free Software Foundation; either
|
||||||
|
version 2.1 of the License, or (at your option) any later version.
|
||||||
|
|
||||||
|
This library is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||||
|
Lesser General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Lesser General Public
|
||||||
|
License along with this library; if not, write to the Free Software
|
||||||
|
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301
|
||||||
|
USA
|
||||||
|
|
||||||
|
Also add information on how to contact you by electronic and paper mail.
|
||||||
|
|
||||||
|
You should also get your employer (if you work as a programmer) or your
|
||||||
|
school, if any, to sign a "copyright disclaimer" for the library, if
|
||||||
|
necessary. Here is a sample; alter the names:
|
||||||
|
|
||||||
|
Yoyodyne, Inc., hereby disclaims all copyright interest in the
|
||||||
|
library `Frob' (a library for tweaking knobs) written by James Random
|
||||||
|
Hacker.
|
||||||
|
|
||||||
|
<signature of Ty Coon>, 1 April 1990
|
||||||
|
Ty Coon, President of Vice
|
||||||
|
|
||||||
|
That's all there is to it!
|
||||||
209
submodules/ModernZ/README.md
Normal file
209
submodules/ModernZ/README.md
Normal file
@@ -0,0 +1,209 @@
|
|||||||
|
<h1 align="center">ModernZ - A Sleek Alternative OSC for mpv</h1>
|
||||||
|
|
||||||
|
A sleek and modern OSC for [mpv](https://mpv.io/), this project is a fork of ModernX designed to enhance functionality by adding more features, all while preserving the core standards of mpv's OSC.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
<p align="center">
|
||||||
|
<a href="#installation"><strong>Installation »</strong></a>
|
||||||
|
<br>
|
||||||
|
<a href="#configuration">Configuration</a>
|
||||||
|
·
|
||||||
|
<a href="#controls">Controls</a>
|
||||||
|
·
|
||||||
|
<a href="#interactive-menus">Interactive Menus</a>
|
||||||
|
·
|
||||||
|
<a href="#translations">OSC Language</a>
|
||||||
|
·
|
||||||
|
<a href="#extras">Extra Scripts</a>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- 🎨 Modern, customizable interface [[options](#configuration)]
|
||||||
|
- 📷 Image Viewer mode with zoom controls [[details](/docs/IMAGE_VIEWER.md)]
|
||||||
|
- 🎛️ Buttons: download, playlist, speed control, screenshot, pin, loop, cache and more. [[details](/docs/CONTROLS.md)]
|
||||||
|
- 📄 Interactive menus for playlist, subtitles, chapters, audio tracks and audio devices [[details](#interactive-menus)]
|
||||||
|
- 🌐 Multi-language support with JSON [locale](#translations) integration
|
||||||
|
- ⌨️ Configurable controls [[details](#controls)]
|
||||||
|
- 🖼️ Video thumbnail previews with [thumbfast](https://github.com/po5/thumbfast)
|
||||||
|
|
||||||
|
## Customization
|
||||||
|
|
||||||
|
You can change the icon theme to `fluent` or `material` to match your taste and style by using the `icon_theme` option in your `modernz.conf`
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
Not a fan of white buttons and text? You have complete control to customize colors and button layouts to perfectly reflect your style.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
See the [Color Customization](docs/USER_OPTS.md#colors-and-style) section in the configuration guide for details on how to customize colors and buttons.
|
||||||
|
|
||||||
|
## Interactive Menus
|
||||||
|
|
||||||
|
ModernZ supports mpv's built-in console/select ([v0.40+](https://github.com/mpv-player/mpv/releases/tag/v0.40.0)) for playlist, subtitles, chapter navigation and more.
|
||||||
|
|
||||||
|
[](/docs/INTERACTIVE_MENUS.md)
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
1. **Disable Stock OSC**
|
||||||
|
|
||||||
|
- Add `osc=no` in your `mpv.conf`
|
||||||
|
|
||||||
|
2. **Copy Files**
|
||||||
|
|
||||||
|
- Place `modernz.lua` in your mpv scripts directory
|
||||||
|
- Place `fluent-system-icons.ttf` and `material-design-icons.ttf` in your mpv fonts directory
|
||||||
|
- (OPTIONAL) Place `thumbfast.lua` in your mpv scripts directory
|
||||||
|
|
||||||
|
3. **Locations**
|
||||||
|
|
||||||
|
```
|
||||||
|
Linux: ~/.config/mpv/
|
||||||
|
Windows: C:/Users/%username%/AppData/Roaming/mpv/
|
||||||
|
macOS: ~/Library/Application Support/mpv/
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **Folder Structure** [[mpv manual](https://mpv.io/manual/master/#files)]
|
||||||
|
```
|
||||||
|
mpv/
|
||||||
|
├── fonts/
|
||||||
|
│ ├── fluent-system-icons.ttf
|
||||||
|
│ └── material-design-icons.ttf
|
||||||
|
├── script-opts/
|
||||||
|
│ └── modernz.conf
|
||||||
|
└── scripts/
|
||||||
|
├── modernz.lua
|
||||||
|
└── thumbfast.lua (optional)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
1. Create `modernz.conf` in the `/script-opts` folder to customize settings
|
||||||
|
|
||||||
|
- [Download default modernz.conf](./modernz.conf)
|
||||||
|
|
||||||
|
2. Example of a short configuration:
|
||||||
|
|
||||||
|
```EditorConfig
|
||||||
|
# Seekbar color (hex format)
|
||||||
|
seekbarfg_color=#B7410E
|
||||||
|
|
||||||
|
# Interface options
|
||||||
|
playlist_button=no
|
||||||
|
title=${media-title}
|
||||||
|
scalewindowed=1.0
|
||||||
|
window_top_bar=auto
|
||||||
|
```
|
||||||
|
|
||||||
|
For a full list of options, [check out the detailed list here](docs/USER_OPTS.md).
|
||||||
|
|
||||||
|
## Controls
|
||||||
|
|
||||||
|
### Button Interactions
|
||||||
|
|
||||||
|
- Left click: Primary action
|
||||||
|
- Right click: Secondary action
|
||||||
|
- Middle click/Shift+Left click: Alternative action
|
||||||
|
|
||||||
|
> [!NOTE]
|
||||||
|
> Middle clicking performs the same function as `Shift+left mouse button`, allowing for one handed use
|
||||||
|
|
||||||
|
For a full list of interactions, check out the [Button Interactions Guide](docs/CONTROLS.md).
|
||||||
|
|
||||||
|
### Keybinds
|
||||||
|
|
||||||
|
ModernZ doesn't set keybinds by default to avoid interfering with your current setup. You can add keybinds in `input.conf` if you prefer:
|
||||||
|
|
||||||
|
```
|
||||||
|
w script-binding modernz/progress-toggle # Toggle progress bar
|
||||||
|
x script-message-to modernz osc-show # Show OSC
|
||||||
|
y script-message-to modernz osc-visibility cycle # Cycle visibility modes
|
||||||
|
z script-message-to modernz osc-idlescreen # Toggle idle screen
|
||||||
|
```
|
||||||
|
|
||||||
|
## Translations
|
||||||
|
|
||||||
|
ModernZ is currently available in English, but you can easily switch it to your preferred language! Here's how:
|
||||||
|
|
||||||
|
1. **Download the locale pack:**
|
||||||
|
|
||||||
|
Grab the [modernz-locale.json](/extras/locale/modernz-locale.json) file from this repository. This file holds translations for various languages.
|
||||||
|
|
||||||
|
2. **Add the locales to mpv:**
|
||||||
|
|
||||||
|
Copy the downloaded `modernz-locale.json` file to your mpv's `/script-opts` folder.
|
||||||
|
|
||||||
|
3. **Choose your language:**
|
||||||
|
|
||||||
|
There are two ways to set your preferred language:
|
||||||
|
|
||||||
|
- **Recommended:** Use the `modernz.conf` file
|
||||||
|
|
||||||
|
```ini
|
||||||
|
# Example configuration in modernz.conf
|
||||||
|
# Set language to Simplified Chinese
|
||||||
|
language=zh
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Alternative:** Edit the `modernz.lua` script
|
||||||
|
|
||||||
|
Open `modernz.lua` and find the `user_opts` section near the beginning. Change the `language` value to your preferred code:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local user_opts = {
|
||||||
|
-- General
|
||||||
|
language = "en", -- Change this to your preferred language code
|
||||||
|
...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Need More Info?**
|
||||||
|
|
||||||
|
For a complete list of available languages, contribution guidelines, and in-depth translation documentation, head over to the [TRANSLATIONS.md](docs/TRANSLATIONS.md).
|
||||||
|
|
||||||
|
## Extras
|
||||||
|
|
||||||
|
The following scripts are ones I wrote and maintain, feel free to use them if they're useful to you.
|
||||||
|
|
||||||
|
- [Pause-Indicator-Lite](/extras/pause-indicator-lite) - A simple script that displays an indicator on pause
|
||||||
|
|
||||||
|
- [ytdlAutoFormat](https://github.com/Samillion/mpv-ytdlautoformat) - A simple mpv script to automatically change `ytdl-format` (yt-dlp) for specified domains.
|
||||||
|
|
||||||
|
- [BoxtoWide](https://github.com/Samillion/mpv-boxtowide) - A simple mpv script to change 4:3 aspect-ratio of video files/streams to 16:9 automatically.
|
||||||
|
|
||||||
|
For even more useful scripts, check out the [mpv User Scripts Wiki](https://github.com/mpv-player/mpv/wiki/User-Scripts). It offers a wide range of community-contributed scripts to enhance your mpv experience.
|
||||||
|
|
||||||
|
## History
|
||||||
|
|
||||||
|
- [Samillion/ModernZ](https://github.com/Samillion/ModernZ)
|
||||||
|
- forked from [dexeonify/ModernX](https://github.com/dexeonify/mpv-config/blob/main/scripts/modernx.lua)
|
||||||
|
- forked from [cyl0/ModernX](https://github.com/cyl0/ModernX)
|
||||||
|
- forked from [maoiscat/mpv-osc-modern](https://github.com/maoiscat/mpv-osc-modern)
|
||||||
|
|
||||||
|
**Why fork yet again?**
|
||||||
|
|
||||||
|
- To add many features in: [Color Customization](docs/USER_OPTS.md#colors-and-style), [Options](docs/USER_OPTS.md) and [Locale Integration](docs/TRANSLATIONS.md)
|
||||||
|
- To integrate console and select into the osc, which inspired mpv to apply it in the stock osc. [ref [#1](https://github.com/mpv-player/mpv/pull/15016), [#2](https://github.com/mpv-player/mpv/pull/15031)]
|
||||||
|
- To add a dedicated layout for images. [[details](/docs/IMAGE_VIEWER.md)]
|
||||||
|
- To re-do the project entirely to match mpv's stock osc standards, to ensure compatibility
|
||||||
|
- To eliminate old bugs and redundancy within the code
|
||||||
|
- Which allows other `Modern` forks to use ModernZ as a base, such as [zydezu/ModernX](https://github.com/zydezu/ModernX). [[Reference](https://github.com/zydezu/ModernX/releases/tag/0.3.9)]
|
||||||
|
|
||||||
|
In essence, to maintain and revive the `modern-osc` origin.
|
||||||
|
|
||||||
|
Having said that, ModernZ still uses parts of the old code, and every previous and current fork author and contributor deserve credit (including mpv's stock osc), that is why they're mentioned in detail.
|
||||||
|
|
||||||
|
#### Credits:
|
||||||
|
|
||||||
|
- Font: UI Fluent System Icons [[details](https://github.com/microsoft/fluentui-system-icons)] [[font file](https://github.com/Samillion/ModernZ/blob/main/fluent-system-icons.ttf)]
|
||||||
|
- Font modified by [Xurdejl](https://github.com/Xurdejl) for use on ModernZ osc
|
||||||
|
- [mpv](https://github.com/mpv-player/mpv) and their [osc.lua](https://github.com/mpv-player/mpv/blob/master/player/lua/osc.lua), as ModernZ osc was re-based on the stock osc standards and applies updates from it
|
||||||
|
- All modern osc origin and their forks as mentioned in [history](#history)
|
||||||
|
- All [contributors](https://github.com/Samillion/ModernZ/graphs/contributors), testers and users that helped directly or indirectly with ModernZ osc ❤️
|
||||||
130
submodules/ModernZ/docs/CONTROLS.md
Normal file
130
submodules/ModernZ/docs/CONTROLS.md
Normal file
@@ -0,0 +1,130 @@
|
|||||||
|
# ModernZ Controls Guide
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## Button Interactions
|
||||||
|
|
||||||
|
Below is a list that explains the OSC buttons function depending on how you interact with them.
|
||||||
|
|
||||||
|
> [!NOTE]
|
||||||
|
> Middle clicking performs the same function as `Shift+left mouse button`, allowing for one handed use
|
||||||
|
|
||||||
|
### Title
|
||||||
|
|
||||||
|
| Action | Function |
|
||||||
|
| ----------------- | ------------------------- |
|
||||||
|
| Left mouse click | Show `media-title` in OSD |
|
||||||
|
| Right mouse click | Show `filename` in OSD |
|
||||||
|
|
||||||
|
### Seekbar
|
||||||
|
|
||||||
|
| Action | Function |
|
||||||
|
| ---------------------- | ---------------------------------- |
|
||||||
|
| Left mouse click | Seek to position (using keyframes) |
|
||||||
|
| Shift+left mouse click | Seek to the exact position |
|
||||||
|
| Right mouse click | Seek to the head of chosen chapter |
|
||||||
|
| Scroll wheel | Seek forward/backwards |
|
||||||
|
|
||||||
|
### Play Time Codes
|
||||||
|
|
||||||
|
| Action | Function |
|
||||||
|
| ----------------- | -------------------------------------------- |
|
||||||
|
| Left mouse click | Display remaining time instead of total time |
|
||||||
|
| Right mouse click | Display time in milliseconds |
|
||||||
|
|
||||||
|
### Play/Pause
|
||||||
|
|
||||||
|
| Action | Function |
|
||||||
|
| ----------------- | -------------------------- |
|
||||||
|
| Left mouse click | Toggle play/pause |
|
||||||
|
| Right mouse click | Toggle enable/disable loop |
|
||||||
|
|
||||||
|
### Playlist Track Back/Forward
|
||||||
|
|
||||||
|
| Action | Function |
|
||||||
|
| ---------------------- | ----------------------- |
|
||||||
|
| Left mouse click | Play previous/next file |
|
||||||
|
| Right mouse click | Show a simple playlist |
|
||||||
|
| Shift+left mouse click | Show a simple playlist |
|
||||||
|
|
||||||
|
### Skip Chapter Back/Forward
|
||||||
|
|
||||||
|
| Action | Function |
|
||||||
|
| ----------------------- | ------------------------------------- |
|
||||||
|
| Left mouse click | Go to previous/next chapter |
|
||||||
|
| Right mouse click | Show an interactive chapters list |
|
||||||
|
| Shift+left mouse click | Jump forwards/backwards by 60 seconds |
|
||||||
|
| Shift+right mouse click | Show a simple chapters list |
|
||||||
|
|
||||||
|
### Jump Back/Forward
|
||||||
|
|
||||||
|
| Action | Function |
|
||||||
|
| ---------------------- | ----------------------------------------------------------------------- |
|
||||||
|
| Left mouse click | Jumps forwards/backwards by 10 seconds (or by `user_opts` `jumpamount`) |
|
||||||
|
| Right mouse click | Jumps forwards/backwards by 1 minute |
|
||||||
|
| Shift+left mouse click | Skips to the previous/next frame (and pauses) |
|
||||||
|
|
||||||
|
### Audio/Subtitle
|
||||||
|
|
||||||
|
| Action | Function |
|
||||||
|
| ---------------------- | ----------------------------------- |
|
||||||
|
| Left mouse click | Show an interactive tracks list |
|
||||||
|
| Right mouse click | Cycle audio/subtitle track |
|
||||||
|
| Shift+left mouse click | Show a simple tracks list |
|
||||||
|
| Scroll wheel | Cycle audio/subtitle tracks up/down |
|
||||||
|
|
||||||
|
### Playlist
|
||||||
|
|
||||||
|
| Action | Function |
|
||||||
|
| ----------------- | ---------------------------- |
|
||||||
|
| Left mouse click | Show an interactive playlist |
|
||||||
|
| Right mouse click | Show a simple playlist |
|
||||||
|
|
||||||
|
### Volume
|
||||||
|
|
||||||
|
| Action | Function |
|
||||||
|
| ---------------- | --------------------- |
|
||||||
|
| Left mouse click | Toggle mute on/off |
|
||||||
|
| Scroll wheel | Change volume up/down |
|
||||||
|
|
||||||
|
### Screenshot
|
||||||
|
|
||||||
|
| Action | Function |
|
||||||
|
| ---------------- | ----------------- |
|
||||||
|
| Left mouse click | Take a screenshot |
|
||||||
|
|
||||||
|
### Pin
|
||||||
|
|
||||||
|
| Action | Function |
|
||||||
|
| ----------------- | -------------------------------------- |
|
||||||
|
| Left mouse click | Toggle pin and toggle window border |
|
||||||
|
| Right mouse click | Toggle pin without changing the border |
|
||||||
|
|
||||||
|
### Loop
|
||||||
|
|
||||||
|
| Action | Function |
|
||||||
|
| ----------------- | -------------------- |
|
||||||
|
| Left mouse click | Toggle loop on/off |
|
||||||
|
|
||||||
|
### Speed Control
|
||||||
|
|
||||||
|
| Action | Function |
|
||||||
|
| ---------------------- | ----------------------------------------- |
|
||||||
|
| Left mouse click | Increase playing speed by increments of 1 |
|
||||||
|
| Right mouse click | Reset playing speed to normal |
|
||||||
|
| Scroll wheel | Inc/decrease playing speed by 0.25 |
|
||||||
|
|
||||||
|
### Download
|
||||||
|
|
||||||
|
| Action | Function |
|
||||||
|
| ----------------- | ------------------------------------ |
|
||||||
|
| Left mouse click | Download the current video/image URL |
|
||||||
|
|
||||||
|
## Keybinds
|
||||||
|
|
||||||
|
| Function | Input Command | Description |
|
||||||
|
| ------------------- | -------------------------------------------------- | ------------------------------------------------------------------------ |
|
||||||
|
| Persistent Progress | `w script-binding modernz/progress-toggle` | Toggle persistent progress bar |
|
||||||
|
| Show OSC | `x script-message-to modernz osc-show` | Show OSC on command without needing to move mouse |
|
||||||
|
| OSC Visibility | `y script-message-to modernz osc-visibility cycle` | OSC visibility mode. Accepts `never`, `auto`, `always` and `cycle` |
|
||||||
|
| Logo on Idle | `z script-message-to modernz osc-idlescreen cycle` | The visibility of the mpv logo on idle. Accepts `yes`, `no`, and `cycle` |
|
||||||
37
submodules/ModernZ/docs/FAQ.md
Normal file
37
submodules/ModernZ/docs/FAQ.md
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
# ModernZ FAQ
|
||||||
|
Answers for frequently asked questions and helpful guidelines that might be useful to you. If you can't find your answer here, don't heistate to [open an issue](https://github.com/Samillion/ModernZ/issues/new/choose).
|
||||||
|
|
||||||
|
## Personalized requests
|
||||||
|
You can most likely achieve this by implementing an auto profile in your `mpv.conf` that applies specific ModernZ user options in certain or specific scenarios.
|
||||||
|
|
||||||
|
**Auto-profile examples:**
|
||||||
|
- [Custom fullscreen](https://github.com/Samillion/ModernZ/issues/206#issuecomment-2470315786)
|
||||||
|
- [PIP mode](https://github.com/Samillion/ModernZ/issues/91#issuecomment-2442569584)
|
||||||
|
- [Image viewer mode](https://github.com/Samillion/ModernZ/blob/main/docs/IMAGE_VIEWER.md#recommended)
|
||||||
|
- [Custom mode](https://github.com/Samillion/ModernZ/blob/main/docs/USER_OPTS.md#auto-profile)
|
||||||
|
|
||||||
|
## Issues or bug reports
|
||||||
|
- ModernZ is compatible with the latest mpv builds, please make sure to update your mpv to see if the issue persists
|
||||||
|
- Search open and closed issues to see if it has been posted before and resolved or not
|
||||||
|
- Make sure that the issue cannot be reproduced on mpv's stock OSC
|
||||||
|
- When posting a bug report, please include a log file with the issue reproduced so we can track the issue the best we can.
|
||||||
|
- Log file: `mpv sample.mp4 --log-file=output.txt` [[details](https://mpv.io/manual/master/#options-log-file)]
|
||||||
|
- Attach the log file, do not paste the text within the issue
|
||||||
|
- Describe the issue in detail as much as you can, preferably with exact steps on how to re-produce the issue.
|
||||||
|
|
||||||
|
## Feature requests
|
||||||
|
Many features and options were added compared to the `modern` origin osc, as shown in [features](/#features) and [user options](/docs/USER_OPTS.md) page.
|
||||||
|
|
||||||
|
We have reached a point that we've become very picky on adding new options, features or adjusting osc behavior, as it can be overwhelming for both users and maintainers if too many were added or adjusted.
|
||||||
|
|
||||||
|
That doesn't mean we won't add anything new, it just means that we're trying to be practical and realistic. Whether the change will benefit all, or a very specific use case.
|
||||||
|
|
||||||
|
## Useful links
|
||||||
|
- [ModernZ controls manual ](/docs/CONTROLS.md)
|
||||||
|
- [ModernZ user options manual](/docs/USER_OPTS.md)
|
||||||
|
- [ModernZ translations and locales](/docs/TRANSLATIONS.md)
|
||||||
|
- [ModernZ extra scripts](/#extras)
|
||||||
|
- [ModernZ history](/#history)
|
||||||
|
- [mpv manual](https://mpv.io/manual/master/)
|
||||||
|
|
||||||
|
Thank you.
|
||||||
56
submodules/ModernZ/docs/IMAGE_VIEWER.md
Normal file
56
submodules/ModernZ/docs/IMAGE_VIEWER.md
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
# ModernZ Image Viewer
|
||||||
|
|
||||||
|
A minimal layout is automatically applied when viewing images while using ModernZ osc, which also features zoom controls.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## Zoom Controls
|
||||||
|
|
||||||
|
Below is a list that explains the zoom control functions depending on how you interact with them.
|
||||||
|
|
||||||
|
| Action | Function |
|
||||||
|
| ----------------- | -------------------------- |
|
||||||
|
| Left mouse click | Zoom in/out of image |
|
||||||
|
| Right mouse click | Reset zoom level to normal |
|
||||||
|
| Mouse Wheel | Zoom in/out of image |
|
||||||
|
|
||||||
|
## Recommended
|
||||||
|
The following options will help you enhance and automate your image viewing experience with mpv. Feel free to adjust them to your liking or use case.
|
||||||
|
|
||||||
|
Reset values per viewed file, should be added in top level within `mpv.conf`:
|
||||||
|
```EditorConfig
|
||||||
|
reset-on-next-file=video-zoom,panscan,video-unscaled,video-rotate,video-align-x,video-align-y
|
||||||
|
```
|
||||||
|
|
||||||
|
[Auto profiles](https://mpv.io/manual/master/#conditional-auto-profiles) that apply useful [mpv options](https://mpv.io/manual/master/#options) and [ModernZ options](/docs/USER_OPTS.md) when viewing images:
|
||||||
|
```EditorConfig
|
||||||
|
[video]
|
||||||
|
profile-cond=p["current-tracks/video"] and not p["current-tracks/video"].image
|
||||||
|
profile-restore=copy-equal
|
||||||
|
taskbar-progress=yes
|
||||||
|
|
||||||
|
[image]
|
||||||
|
profile-desc=ModernZ osc Image Viewer mode
|
||||||
|
profile-cond=p["current-tracks/video"] and p["current-tracks/video"].image and not p["current-tracks/video"].albumart
|
||||||
|
profile-restore=copy-equal
|
||||||
|
video-recenter=yes
|
||||||
|
taskbar-progress=no
|
||||||
|
stop-screensaver=no
|
||||||
|
prefetch-playlist=yes
|
||||||
|
video-aspect-override=no
|
||||||
|
image-display-duration=inf
|
||||||
|
title=${media-title} [${?width:${width}x${height}}]
|
||||||
|
script-opts-append=modernz-fade_alpha=50
|
||||||
|
script-opts-append=modernz-window_title=yes
|
||||||
|
script-opts-append=modernz-bottomhover_zone=50
|
||||||
|
script-opts-append=modernz-windowcontrols_title=${media-title} [${?width:${width}x${height}}]
|
||||||
|
```
|
||||||
|
|
||||||
|
## Useful
|
||||||
|
If you want to extend functionality even further to turn mpv into a full fledged image viewer, we recommend the following Github repository:
|
||||||
|
- https://github.com/guidocella/mpv-image-config
|
||||||
|
|
||||||
|
It allows you to have options such as:
|
||||||
|
- Cursor centeric zoom with scroll wheel
|
||||||
|
- Pan image, drag to pan and use gestures
|
||||||
|
- Double page mode (ie: manga)
|
||||||
38
submodules/ModernZ/docs/INTERACTIVE_MENUS.md
Normal file
38
submodules/ModernZ/docs/INTERACTIVE_MENUS.md
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
## Interactive Menus
|
||||||
|
ModernZ integrates mpv's [console.lua](https://github.com/mpv-player/mpv/blob/master/player/lua/console.lua) and [select.lua](https://github.com/mpv-player/mpv/blob/master/player/lua/select.lua), which have been built in mpv starting v0.39+. Can be used in:
|
||||||
|
- Playlist button
|
||||||
|
- Subtitles button
|
||||||
|
- Audio track button
|
||||||
|
- Chapter title (chapters list)
|
||||||
|
- Volume control button (audio device selector) [right mouse button]
|
||||||
|
|
||||||
|
For details on how to interact with these buttons, view the [controls manual](/docs/CONTROLS.md).
|
||||||
|
|
||||||
|
https://github.com/user-attachments/assets/a7330f29-c26d-4f5a-8797-d20f97211fdc
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
> [!IMPORTANT]
|
||||||
|
> If the menus are not functioning as shown in the video, it is most likely because you're using an outdated version of mpv
|
||||||
|
|
||||||
|
- Check [mpv installation sources](https://mpv.io/installation/) on the official website for up-to-date packages
|
||||||
|
- Use [mpv-build](https://github.com/mpv-player/mpv-build) to get the latest release
|
||||||
|
- Use a simplified alternative function as shown below until you're able to update mpv
|
||||||
|
|
||||||
|
#### Alternative function: (for outdated mpv)
|
||||||
|
You can add the following to `modernz.conf` in your `script-opts` folder to at least get some functionality until you update mpv
|
||||||
|
|
||||||
|
```EditorConfig
|
||||||
|
title_mbtn_left_command=show-text ${filename}
|
||||||
|
playlist_mbtn_left_command=show-text ${playlist} 3000
|
||||||
|
audio_track_mbtn_left_command=cycle audio
|
||||||
|
sub_track_mbtn_left_command=cycle sub
|
||||||
|
chapter_title_mbtn_left_command=show-text ${chapter-list} 3000
|
||||||
|
```
|
||||||
|
|
||||||
|
> [!TIP]
|
||||||
|
> If the font size for the interactive playlist is too small, you can add the following in your `mpv.conf`
|
||||||
|
>
|
||||||
|
> ```ini
|
||||||
|
> # change font size for console and select
|
||||||
|
> script-opts-append=console-font_size=25
|
||||||
|
> ```
|
||||||
141
submodules/ModernZ/docs/TRANSLATIONS.md
Normal file
141
submodules/ModernZ/docs/TRANSLATIONS.md
Normal file
@@ -0,0 +1,141 @@
|
|||||||
|
# ModernZ Translations Guide
|
||||||
|
|
||||||
|
This guide provides everything you need to use, modify, and add translations for the ModernZ OSC interface.
|
||||||
|
|
||||||
|
## Table of Contents
|
||||||
|
|
||||||
|
- [Quick Start](#quick-start)
|
||||||
|
- [Available Languages](#available-languages)
|
||||||
|
- [Adding a New Language](#adding-a-new-language)
|
||||||
|
- [Updating Existing Translations](#updating-existing-translations)
|
||||||
|
- [Translation Keys Reference](#translation-keys-reference)
|
||||||
|
- [Contributing Translations](#contributing)
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
To get started with translations:
|
||||||
|
|
||||||
|
1. **Locate the translation file:** Open the `modernz-locale.json` file from the repository (typically found at [modernz-locale.json](/extras/locale/modernz-locale.json)).
|
||||||
|
2. **Edit Translations:** Use the following format to add or modify translations:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"language-code": {
|
||||||
|
"idle": "Your translation",
|
||||||
|
"na": "Your translation",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Replace `"language-code"` with your language code (e.g. `"es"` for Spanish).
|
||||||
|
|
||||||
|
## Available Languages
|
||||||
|
|
||||||
|
ModernZ currently has translations for the following languages:
|
||||||
|
|
||||||
|
- French (fr)
|
||||||
|
- German (de)
|
||||||
|
- Spanish (es)
|
||||||
|
- Polish (pl)
|
||||||
|
- Japanese (jp)
|
||||||
|
- Simplified Chinese (zh)
|
||||||
|
- Arabic (ar)
|
||||||
|
- Russian (ru)
|
||||||
|
|
||||||
|
## Adding a New Language
|
||||||
|
|
||||||
|
1. **Get the translation file**: If you haven't already, download the `modernz-locale.json` file.
|
||||||
|
2. **Open the file**: Open `modernz-locale.json` in a text editor.
|
||||||
|
3. **Choose a language code**: Pick a unique code representing your language (e.g. `"it"` for Italian).
|
||||||
|
4. **Copy an existing template**: Use an existing language section as a base. For example, use the English template:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"en": {
|
||||||
|
"idle": "Drop files or URLs here to play",
|
||||||
|
"na": "Not available",
|
||||||
|
"video": "Video",
|
||||||
|
"audio": "Audio",
|
||||||
|
"subtitle": "Subtitle",
|
||||||
|
"no_subs": "No subtitles available",
|
||||||
|
"no_audio": "No audio tracks available",
|
||||||
|
"playlist": "Playlist",
|
||||||
|
"no_playlist": "Playlist is empty",
|
||||||
|
"chapter": "Chapter",
|
||||||
|
"ontop": "Pin Window",
|
||||||
|
"ontop_disable": "Unpin Window",
|
||||||
|
"loop_enable": "Loop",
|
||||||
|
"loop_disable": "Disable Loop",
|
||||||
|
"speed_control": "Speed Control",
|
||||||
|
"screenshot": "Screenshot",
|
||||||
|
"stats_info": "Information",
|
||||||
|
"cache": "Cache",
|
||||||
|
"buffering": "Buffering",
|
||||||
|
"zoom_in": "Zoom In",
|
||||||
|
"zoom_out": "Zoom Out",
|
||||||
|
"download": "Download",
|
||||||
|
"download_in_progress": "Download in progress",
|
||||||
|
"downloading": "Downloading",
|
||||||
|
"downloaded": "Already downloaded",
|
||||||
|
"menu": "Menu",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
5. **Translate the strings**: Replace the English text with your translations, keeping the same keys.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"es": {
|
||||||
|
"idle": "Suelta archivos o URLs aquí para reproducirlos",
|
||||||
|
"na": "No disponible",
|
||||||
|
"video": "Video",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Updating existing translations
|
||||||
|
|
||||||
|
1. **Find your language**: Locate the section for your language in `modernz-locale.json`.
|
||||||
|
|
||||||
|
2. **Update the strings**
|
||||||
|
|
||||||
|
3. **Test your changes**: Play a video to verify the translations work correctly.
|
||||||
|
|
||||||
|
## Translation Keys Reference
|
||||||
|
|
||||||
|
| Key | Description | Format Notes |
|
||||||
|
| ------------------------ | ----------------------- | --------------------------- |
|
||||||
|
| idle | Idle state message | Be brief |
|
||||||
|
| na | Not available | Menu label |
|
||||||
|
| video | Video label | Menu label |
|
||||||
|
| audio | Audio label | Menu label |
|
||||||
|
| subtitle | Subtitle label | Menu label |
|
||||||
|
| no_subs | No subtitles label | Menu label |
|
||||||
|
| no_audio | No audio label | Menu label |
|
||||||
|
| playlist | Playlist label | Menu label |
|
||||||
|
| no_playlist | No playlist label | Menu label |
|
||||||
|
| chapter | Chapter label | Menu label |
|
||||||
|
| ontop/ontop_disable | Window pin states | Action labels |
|
||||||
|
| loop_enable/loop_disable | Loop states | Action labels |
|
||||||
|
| screenshot | Screenshot label | Action label |
|
||||||
|
| stats_info | Information label | Menu label |
|
||||||
|
| cache | Information label | Text label |
|
||||||
|
| buffering | Information label | Text label |
|
||||||
|
| zoom_in | Information label | Menu label |
|
||||||
|
| zoom_out | Information label | Menu label |
|
||||||
|
| download | Information label | Menu label |
|
||||||
|
| download_in_progress | Information label | Menu label |
|
||||||
|
| downloading | Information label | Menu label |
|
||||||
|
| downloaded | Information label | Menu label |
|
||||||
|
| menu | Information label | Menu label |
|
||||||
|
|
||||||
|
## Contributing
|
||||||
|
|
||||||
|
If you'd like to share your translations with the community:
|
||||||
|
|
||||||
|
1. **Fork the repository**: Visit the ModernZ GitHub page (https://github.com/Samillion/ModernZ) and click "Fork" to create your own copy of the project.
|
||||||
|
2. **Edit the** `modernz-locale.json` **file** Add your translations to the file in your forked repository.
|
||||||
|
3. **Submit a Pull Request** Create a pull request on GitHub to propose your changes for review and integration.
|
||||||
|
|
||||||
|
Thank you for contributing ❤️ Every translation helps make the project better!
|
||||||
276
submodules/ModernZ/docs/USER_OPTS.md
Normal file
276
submodules/ModernZ/docs/USER_OPTS.md
Normal file
@@ -0,0 +1,276 @@
|
|||||||
|
# ModernZ User Options Guide
|
||||||
|
|
||||||
|
### Configuration File Location
|
||||||
|
|
||||||
|
Create `modernz.conf` in your mpv script-opts directory:
|
||||||
|
|
||||||
|
- Linux: `~/.config/mpv/script-opts/`
|
||||||
|
- Windows: `%APPDATA%/mpv/script-opts/`
|
||||||
|
- macOS: `~/Library/Application Support/mpv/script-opts/`
|
||||||
|
|
||||||
|
## Available Options
|
||||||
|
|
||||||
|
### Language and display
|
||||||
|
|
||||||
|
| Option | Value | Description |
|
||||||
|
| -------------- | --------------- | ------------------------------------------------------------------------------------------------------------------------------ |
|
||||||
|
| language | en | set language (for available options, see: [Translations](https://github.com/Samillion/ModernZ/blob/main/docs/TRANSLATIONS.md)) |
|
||||||
|
| icon_theme | fluent | set icon theme. accepts `fluent` or `material` |
|
||||||
|
| font | mpv-osd-symbols | font for the OSC (default: mpv-osd-symbols or the one set in mpv.conf) |
|
||||||
|
| idlescreen | yes | show mpv logo when idle |
|
||||||
|
| window_top_bar | auto | show OSC window top bar: `"auto"`, `"yes"`, or `"no"` (borderless/fullscreen) |
|
||||||
|
| showwindowed | yes | show OSC when windowed |
|
||||||
|
| showfullscreen | yes | show OSC when fullscreen |
|
||||||
|
| showonpause | yes | show OSC when paused |
|
||||||
|
| keeponpause | yes | disable OSC hide timeout when paused |
|
||||||
|
| greenandgrumpy | no | disable Santa hat in December |
|
||||||
|
|
||||||
|
### OSC behaviour and scaling
|
||||||
|
|
||||||
|
| Option | Value | Description |
|
||||||
|
| ----------------------- | ----- | ---------------------------------------------------------------------- |
|
||||||
|
| hidetimeout | 2000 | time (in ms) before OSC hides if no mouse movement |
|
||||||
|
| seek_resets_hidetimeout | yes | if seeking should reset the hidetimeout |
|
||||||
|
| fadeduration | 200 | fade-out duration (in ms), set to `"0"` for no fade |
|
||||||
|
| fadein | no | whether to enable fade-in effect |
|
||||||
|
| minmousemove | 0 | minimum mouse movement (in pixels) required to show OSC |
|
||||||
|
| bottomhover | yes | show OSC only when hovering at the bottom |
|
||||||
|
| bottomhover_zone | 130 | height of hover zone for bottomhover (in pixels) |
|
||||||
|
| osc_on_seek | no | show OSC when seeking |
|
||||||
|
| osc_on_start | no | show OSC on start of every file |
|
||||||
|
| mouse_seek_pause | yes | pause video while seeking with mouse move (on button hold) |
|
||||||
|
| force_seek_tooltip | no | force show seekbar tooltip on mouse drag, even if not hovering seekbar |
|
||||||
|
| vidscale | auto | scale osc with the video. (set to `"no"` to disable) |
|
||||||
|
| scalewindowed | 1.0 | osc scale factor when windowed |
|
||||||
|
| scalefullscreen | 1.0 | osc scale factor when fullscreen |
|
||||||
|
|
||||||
|
### Elements display
|
||||||
|
|
||||||
|
| Option | Value | Description |
|
||||||
|
| ----------------------- | ---------------- | -------------------------------------------------------------------------------- |
|
||||||
|
| show_title | yes | show title in the OSC (above seekbar) |
|
||||||
|
| title | `${media-title}` | title above seekbar format: `"${media-title}"` or `"${filename}"` |
|
||||||
|
| title_font_size | 24 | font size of the title text (above seekbar) |
|
||||||
|
| chapter_title_font_size | 14 | chapter title font size |
|
||||||
|
| show_chapter_title | yes | show chapter title (above seekbar) |
|
||||||
|
| chapter_fmt | %s | format for chapter display on seekbar hover (set to `"no"` to disable) |
|
||||||
|
| timetotal | yes | show total time instead of remaining time |
|
||||||
|
| timems | no | show timecodes with milliseconds |
|
||||||
|
| unicodeminus | no | use the Unicode minus sign in remaining time |
|
||||||
|
| time_format | dynamic | `"dynamic"` or `"fixed"`. shows MM:SS when possible, fixed always shows HH:MM:SS |
|
||||||
|
| time_font_size | 16 | font size of the time display |
|
||||||
|
| cache_info | no | show cached time information |
|
||||||
|
| cache_info_speed | no | show current cache speed per second |
|
||||||
|
| cache_info_font_size | 12 | font size of the time display |
|
||||||
|
| tooltip_font_size | 14 | tooltips font size |
|
||||||
|
|
||||||
|
### Title bar settings
|
||||||
|
|
||||||
|
| Option | Value | Description |
|
||||||
|
| -------------------- | ---------------- | ------------------------------------------------------------------------- |
|
||||||
|
| window_title | no | show window title in borderless/fullscreen mode |
|
||||||
|
| window_controls | yes | show window controls (close, minimize, maximize) in borderless/fullscreen |
|
||||||
|
| windowcontrols_title | `${media-title}` | same as title but for window_top_bar |
|
||||||
|
|
||||||
|
### Subtitle display settings
|
||||||
|
|
||||||
|
| Option | Value | Description |
|
||||||
|
| --------------------- | ----- | ---------------------------------------------------------------------- |
|
||||||
|
| raise_subtitles | yes | raise subtitles above the OSC when shown |
|
||||||
|
| raise_subtitle_amount | 125 | amount by which subtitles are raised when the OSC is shown (in pixels) |
|
||||||
|
|
||||||
|
### Buttons display and functionality
|
||||||
|
|
||||||
|
| Option | Value | Description |
|
||||||
|
| -------------------------- | ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||||
|
| jump_buttons | yes | show "jump forward/backward 10 seconds" buttons |
|
||||||
|
| jump_amount | 10 | change the jump amount in seconds |
|
||||||
|
| jump_more_amount | 60 | change the jump amount in seconds when right-clicking jump buttons and shift-clicking chapter skip buttons |
|
||||||
|
| jump_icon_number | yes | show different icon for `5`, `10`, or `30` second jumps |
|
||||||
|
| jump_mode | relative | seek mode for jump buttons |
|
||||||
|
| jump_softrepeat | yes | enable continuous jumping when holding down seek buttons |
|
||||||
|
| chapter_skip_buttons | no | show the skip back and forward (chapter) buttons |
|
||||||
|
| chapter_softrepeat | yes | enable continuous skipping when holding down chapter skip buttons |
|
||||||
|
| track_nextprev_buttons | yes | show next/previous playlist track buttons |
|
||||||
|
| volume_control | yes | show mute button and volume slider |
|
||||||
|
| volume_control_type | linear | volume scale type: `"linear"` or `"logarithmic"` |
|
||||||
|
| playlist_button | yes | show playlist button: Left-click for simple playlist, Right-click for interactive playlist |
|
||||||
|
| hide_empty_playlist_button | yes | hide playlist button when no playlist exists |
|
||||||
|
| gray_empty_playlist_button | yes | gray out the playlist button when no playlist exists |
|
||||||
|
| download_button | yes | show download button on web videos (requires yt-dlp and ffmpeg) |
|
||||||
|
| download_path | ~~desktop/mpv | default download directory for videos. [Learn more about setting paths here](https://mpv.io/manual/master/#paths). |
|
||||||
|
| screenshot_button | no | show screenshot button |
|
||||||
|
| screenshot_flag | subtitles | Flag options for the screenshot button: `"subtitles"`, `"video"`, `"window"`, `"each-frame"`. [Find out more about these options](https://mpv.io/manual/master/#command-interface-screenshot-<flags>). |
|
||||||
|
| ontop_button | yes | show `window on top (pin)` button |
|
||||||
|
| loop_button | no | show `loop` button |
|
||||||
|
| speed_button | no | show speed control button |
|
||||||
|
| speed_button_click | 1 | speed change amount per click |
|
||||||
|
| speed_button_scroll | 0.25 | speed change amount on scroll |
|
||||||
|
| loop_in_pause | yes | enable looping by right-clicking pause |
|
||||||
|
| buttons_always_active | none | force buttons to always be active. can add: `playlist_prev`, `playlist_next` |
|
||||||
|
| info_button | yes | show `info (stats)` button |
|
||||||
|
| fullscreen_button | yes | show `fullscreen toggle` button |
|
||||||
|
| playpause_size | 28 | icon size for the play/pause button |
|
||||||
|
| midbuttons_size | 24 | icon size for the middle buttons |
|
||||||
|
| sidebuttons_size | 24 | icon size for the side buttons |
|
||||||
|
| zoom_control | yes | show zoom controls in image viewer mode |
|
||||||
|
| zoom_in_max | 4 | icon size for the side buttons |
|
||||||
|
| zoom_out_min | -1 | icon size for the side buttons |
|
||||||
|
|
||||||
|
|
||||||
|
### Colors and style
|
||||||
|
|
||||||
|
| Option | Value | Description |
|
||||||
|
| --------------------------------- | --------- | ------------------------------------------------------------------------------------------------- |
|
||||||
|
| osc_color | `#000000` | accent color of the OSC and title bar |
|
||||||
|
| window_title_color | `#FFFFFF` | color of the title in borderless/fullscreen mode |
|
||||||
|
| window_controls_color | `#FFFFFF` | color of the window controls (close, minimize, maximize) in borderless/fullscreen mode |
|
||||||
|
| windowcontrols_close_hover | `#E81123` | color of close window control on hover |
|
||||||
|
| windowcontrols_max_hover | `#F8BC3A` | color of maximize window controls on hover |
|
||||||
|
| windowcontrols_min_hover | `#43CB44` | color of minimize window controls on hover |
|
||||||
|
| title_color | `#FFFFFF` | color of the title (above seekbar) |
|
||||||
|
| seekbarfg_color | `#FB8C00` | color of the seekbar progress and handle |
|
||||||
|
| seekbarbg_color | `#94754F` | color of the remaining seekbar |
|
||||||
|
| seekbar_cache_color | `#918F8E` | color of the cache ranges on the seekbar |
|
||||||
|
| volumebar_match_seek_color | no | match volume bar color with seekbar color (ignores `side_buttons_color`) |
|
||||||
|
| time_color | `#FFFFFF` | color of the timestamps (below seekbar) |
|
||||||
|
| chapter_title_color | `#FFFFFF` | color of the chapter title (above seekbar) |
|
||||||
|
| cache_info_color | `#FFFFFF` | color of the cache information |
|
||||||
|
| side_buttons_color | `#FFFFFF` | color of the side buttons (audio, subtitles, playlist, etc.) |
|
||||||
|
| middle_buttons_color | `#FFFFFF` | color of the middle buttons (skip, jump, chapter, etc.) |
|
||||||
|
| playpause_color | `#FFFFFF` | color of the play/pause button |
|
||||||
|
| held_element_color | `#999999` | color of the element when held down (pressed) |
|
||||||
|
| hover_effect_color | `#FB8C00` | color of a hovered button when `hover_effect` includes `"color"` |
|
||||||
|
| thumbnail_border_color | `#111111` | color of the border for thumbnails (with thumbfast) |
|
||||||
|
| thumbnail_border_outline | `#404040` | color of the border outline for thumbnails |
|
||||||
|
| fade_alpha | 130 | alpha of the OSC background box (0 to disable) |
|
||||||
|
| fade_blur_strength | 100 | blur strength for the OSC alpha fade. caution: high values can take a lot of CPU time to render |
|
||||||
|
| fade_transparency_strength | 0 | use with "fade_blur_strength=0" to create a transparency box |
|
||||||
|
| window_fade_alpha | 100 | alpha of the window title bar (0 to disable) |
|
||||||
|
| window_fade_blur_strength | 100 | blur strength for the window title bar. caution: high values can take a lot of CPU time to render |
|
||||||
|
| window_fade_transparency_strength | 0 | use with "window_fade_blur_strength=0" to create a transparency box |
|
||||||
|
| thumbnail_border | 3 | width of the thumbnail border (for thumbfast) |
|
||||||
|
| thumbnail_border_radius | 3 | rounded corner radius for thumbnail border (0 to disable) |
|
||||||
|
|
||||||
|
### Button hover effects
|
||||||
|
|
||||||
|
| Option | Value | Description |
|
||||||
|
| ------------------------ | --------------- | ------------------------------------------------------------------------------------------------ |
|
||||||
|
| hover_effect | size,glow,color | active button hover effects: `"glow"`, `"size"`, `"color"`; can use multiple separated by commas |
|
||||||
|
| hover_button_size | 115 | relative size of a hovered button if "size" effect is active |
|
||||||
|
| button_glow_amount | 5 | glow intensity when `"glow"` hover effect is active |
|
||||||
|
| hover_effect_for_sliders | yes | apply hover size effect to slider handle |
|
||||||
|
|
||||||
|
### Tooltips and hints
|
||||||
|
|
||||||
|
| Option | Value | Description |
|
||||||
|
| ------------------------------ | ----- | --------------------------------------------------------------- |
|
||||||
|
| tooltips_for_disabled_elements | yes | enable tooltips for disabled buttons and elements |
|
||||||
|
| tooltip_hints | yes | enable text hints for info, loop, ontop, and screenshot buttons |
|
||||||
|
|
||||||
|
### Progress bar settings
|
||||||
|
|
||||||
|
| Option | Value | Description |
|
||||||
|
| ------------------------ | -------- | -------------------------------------------------------------------------- |
|
||||||
|
| seek_handle_size | 0.8 | size ratio of the seekbar handle (range: 0 ~ 1) |
|
||||||
|
| seekrange | yes | show seek range overlay |
|
||||||
|
| seekrangealpha | 150 | transparency of the seek range |
|
||||||
|
| livemarkers | yes | update chapter markers on the seekbar when duration changes |
|
||||||
|
| seekbarkeyframes | no | use keyframes when dragging the seekbar |
|
||||||
|
| nibbles_top | yes | top chapter nibbles above seekbar |
|
||||||
|
| nibbles_bottom | yes | bottom chapter nibbles below seekbar |
|
||||||
|
| nibbles_style | triangle | chapter nibble style. `triangle`, `bar` or `single-bar` |
|
||||||
|
| automatickeyframemode | yes | automatically set keyframes for the seekbar based on video length |
|
||||||
|
| automatickeyframelimit | 600 | videos longer than this (in seconds) will have keyframes on the seekbar |
|
||||||
|
| persistentprogress | no | always show a small progress line at the bottom of the screen |
|
||||||
|
| persistentprogressheight | 17 | height of the persistent progress bar |
|
||||||
|
| persistentbuffer | no | show buffer status on web videos in the persistent progress line |
|
||||||
|
|
||||||
|
### Miscellaneous settings
|
||||||
|
|
||||||
|
| Option | Value | Description |
|
||||||
|
| ----------------------------- | ----------------- | ------------------------------------------------------------- |
|
||||||
|
| visibility | auto | only used at init to set visibility_mode(...) |
|
||||||
|
| visibility_modes | never_auto_always | visibility modes to cycle through, modes are separated by `_` |
|
||||||
|
| tick_delay | 0.03 | minimum interval between OSC redraws (in seconds) |
|
||||||
|
| tick_delay_follow_display_fps | no | use display FPS as the minimum redraw interval |
|
||||||
|
|
||||||
|
### Elements Position
|
||||||
|
|
||||||
|
Useful when adjusting font size or type, this will help you change the affected elements position
|
||||||
|
|
||||||
|
| Option | Value | Description |
|
||||||
|
| ----------------------------- | ------ | ---------------------------------------------------------------------- |
|
||||||
|
| title_height | 96 | title height position above seekbar |
|
||||||
|
| title_with_chapter_height | 108 | title height position if a chapter title is below it |
|
||||||
|
| chapter_title_height | 91 | chapter title height position above seekbar |
|
||||||
|
| time_codes_height | 35 | time codes height position |
|
||||||
|
| time_codes_centered_height | 57 | time codes height position with portrait window |
|
||||||
|
| tooltip_height_offset | 2 | tooltip height position offset |
|
||||||
|
| tooltip_left_offset | 5 | if tooltip contains many characters, it is moved to the left by offset |
|
||||||
|
| portrait_window_trigger | 1000 | portrait window width trigger to move some elements |
|
||||||
|
| hide_volume_bar_trigger | 1150 | hide volume bar trigger window width |
|
||||||
|
| notitle_osc_h_offset | 25 | osc height offset if title above seekbar is disabled |
|
||||||
|
| nochapter_osc_h_offset | 10 | osc height offset if chapter title is disabled or doesn't exist |
|
||||||
|
| seek_hover_tooltip_h_offset | 0 | seek hover timecodes tooltip height position offset |
|
||||||
|
| osc_height | 132 | osc height without offsets |
|
||||||
|
|
||||||
|
### Mouse Commands (User Options)
|
||||||
|
|
||||||
|
Customize the button function based on mouse actions.
|
||||||
|
|
||||||
|
| Type | Option | Function |
|
||||||
|
| ----------------------------- | -------------------------------- | ------------------------------------------------------------------------------- |
|
||||||
|
| Title (above seekbar) | title_mbtn_left_command | `script-binding stats/display-page-5` |
|
||||||
|
| | title_mbtn_mid_command | `show-text ${path}` |
|
||||||
|
| | title_mbtn_right_command | `script-binding select/select-watch-history; script-message-to modernz osc-hide`|
|
||||||
|
| Playlist Button | playlist_mbtn_left_command | `script-binding select/select-playlist; script-message-to modernz osc-hide` |
|
||||||
|
| | playlist_mbtn_right_command | `show-text ${playlist} 3000` |
|
||||||
|
| Volume Control | vol_ctrl_mbtn_left_command | `no-osd cycle mute` |
|
||||||
|
| | vol_ctrl_mbtn_right_command | `script-binding select/select-audio-device; script-message-to modernz osc-hide` |
|
||||||
|
| | vol_ctrl_wheel_down_command | `no-osd add volume -5` |
|
||||||
|
| | vol_ctrl_wheel_up_command | `no-osd add volume 5` |
|
||||||
|
| Audio Button | audio_track_mbtn_left_command | `script-binding select/select-aid; script-message-to modernz osc-hide` |
|
||||||
|
| | audio_track_mbtn_mid_command | `cycle audio down` |
|
||||||
|
| | audio_track_mbtn_right_command | `cycle audio` |
|
||||||
|
| | audio_track_wheel_down_command | `cycle audio` |
|
||||||
|
| | audio_track_wheel_up_command | `cycle audio down` |
|
||||||
|
| Subtitle Button | sub_track_mbtn_left_command | `script-binding select/select-sid; script-message-to modernz osc-hide` |
|
||||||
|
| | sub_track_mbtn_mid_command | `cycle sub down` |
|
||||||
|
| | sub_track_mbtn_right_command | `cycle sub` |
|
||||||
|
| | sub_track_wheel_down_command | `cycle sub` |
|
||||||
|
| | sub_track_wheel_up_command | `cycle sub down` |
|
||||||
|
| Chapter Skip Buttons | chapter_prev_mbtn_left_command | `add chapter -1` |
|
||||||
|
| | chapter_prev_mbtn_mid_command | `show-text ${chapter-list} 3000` |
|
||||||
|
| | chapter_prev_mbtn_right_command | `script-binding select/select-chapter; script-message-to modernz osc-hide` |
|
||||||
|
| | chapter_next_mbtn_left_command | `add chapter 1` |
|
||||||
|
| | chapter_next_mbtn_mid_command | `show-text ${chapter-list} 3000` |
|
||||||
|
| | chapter_next_mbtn_right_command | `script-binding select/select-chapter; script-message-to modernz osc-hide` |
|
||||||
|
| Chapter Title (below seekbar) | chapter_title_mbtn_left_command | `script-binding select/select-chapter; script-message-to modernz osc-hide` |
|
||||||
|
| | chapter_title_mbtn_right_command | `show-text ${chapter-list} 3000` |
|
||||||
|
| Playlist Skip Buttons | playlist_prev_mbtn_left_command | `playlist-prev` |
|
||||||
|
| | playlist_prev_mbtn_mid_command | `show-text ${playlist} 3000` |
|
||||||
|
| | playlist_prev_mbtn_right_command | `script-binding select/select-playlist; script-message-to modernz osc-hide` |
|
||||||
|
| | playlist_next_mbtn_left_command | `playlist-next` |
|
||||||
|
| | playlist_next_mbtn_mid_command | `show-text ${playlist} 3000` |
|
||||||
|
| | playlist_next_mbtn_right_command | `script-binding select/select-playlist; script-message-to modernz osc-hide` |
|
||||||
|
| Fullscreen Button | fullscreen_mbtn_left_command | `cycle fullscreen` |
|
||||||
|
| | fullscreen_mbtn_right_command | `cycle window-maximized` |
|
||||||
|
| Info Button | info_mbtn_left_command | `script-binding stats/display-page-1-toggle` |
|
||||||
|
|
||||||
|
### Auto Profile
|
||||||
|
|
||||||
|
Below is an example of an auto-profile in `mpv.conf` you can use to set any of ModernZ options based on certain conditions, in this case `when window is pinned or fullscreen`.
|
||||||
|
|
||||||
|
```ini
|
||||||
|
[ModernZ-Custom]
|
||||||
|
profile-desc=Apply ModernZ options on pin or fullscreen
|
||||||
|
profile-cond=ontop and ontop == true or fullscreen
|
||||||
|
profile-restore=copy-equal
|
||||||
|
script-opts-append=modernz-persistentprogress=yes
|
||||||
|
script-opts-append=modernz-seekbarfg_color=#FF0000
|
||||||
|
script-opts-append=modernz-bottomhover=no
|
||||||
|
#...etc
|
||||||
|
```
|
||||||
|
|
||||||
|
More information about auto profiles available on [mpv's manual](https://mpv.io/manual/master/#conditional-auto-profiles).
|
||||||
3
submodules/ModernZ/extras/locale/README.md
Normal file
3
submodules/ModernZ/extras/locale/README.md
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
The different locales (languages) for ModernZ OSC are stored within the [modernz-locale.json](/extras/locale/modernz-locale.json) file.
|
||||||
|
|
||||||
|
For more information: [Translations](/docs/TRANSLATIONS.md)
|
||||||
226
submodules/ModernZ/extras/locale/modernz-locale.json
Normal file
226
submodules/ModernZ/extras/locale/modernz-locale.json
Normal file
@@ -0,0 +1,226 @@
|
|||||||
|
{
|
||||||
|
"fr": {
|
||||||
|
"idle": "Déposez des fichiers ou des URLs ici pour les lire",
|
||||||
|
"na": "Non disponible",
|
||||||
|
"video": "Vidéo",
|
||||||
|
"audio": "Audio",
|
||||||
|
"subtitle": "Sous-titre",
|
||||||
|
"no_subs": "Aucun sous-titre disponible",
|
||||||
|
"no_audio": "Aucune piste audio disponible",
|
||||||
|
"playlist": "Liste de lecture",
|
||||||
|
"no_playlist": "Liste de lecture vide",
|
||||||
|
"chapter": "Chapitre",
|
||||||
|
"ontop": "Épingler la fenêtre",
|
||||||
|
"ontop_disable": "Désépingler la fenêtre",
|
||||||
|
"loop_enable": "Activer la boucle",
|
||||||
|
"loop_disable": "Désactiver la boucle",
|
||||||
|
"speed_control": "Contrôle de la vitesse",
|
||||||
|
"screenshot": "Capture d'écran",
|
||||||
|
"stats_info": "Informations",
|
||||||
|
"cache": "Cache",
|
||||||
|
"buffering": "Chargement",
|
||||||
|
"zoom_in": "Agrandir",
|
||||||
|
"zoom_out": "Rétrécir",
|
||||||
|
"download": "Télécharger",
|
||||||
|
"download_in_progress": "Téléchargement en cours",
|
||||||
|
"downloading": "Téléchargement",
|
||||||
|
"downloaded": "Déjà téléchargé",
|
||||||
|
"menu": "Menu",
|
||||||
|
},
|
||||||
|
"de": {
|
||||||
|
"idle": "Dateien oder URLs hier ablegen, um sie abzuspielen",
|
||||||
|
"na": "Nicht verfügbar",
|
||||||
|
"video": "Video",
|
||||||
|
"audio": "Audio",
|
||||||
|
"subtitle": "Untertitel",
|
||||||
|
"no_subs": "Keine Untertitel verfügbar",
|
||||||
|
"no_audio": "Keine Audiospuren verfügbar",
|
||||||
|
"playlist": "Wiedergabeliste",
|
||||||
|
"no_playlist": "Wiedergabeliste ist leer",
|
||||||
|
"chapter": "Kapitel",
|
||||||
|
"ontop": "Fenster fixieren",
|
||||||
|
"ontop_disable": "Fensterfixierung aufheben",
|
||||||
|
"loop_enable": "Dauerschleife aktivieren",
|
||||||
|
"loop_disable": "Dauerschleife deaktivieren",
|
||||||
|
"speed_control": "Geschwindigkeitseinstellungen",
|
||||||
|
"screenshot": "Bildschirmaufnahme",
|
||||||
|
"stats_info": "Informationen",
|
||||||
|
"cache": "Zwischenspeicher",
|
||||||
|
"buffering": "Pufferung",
|
||||||
|
"zoom_in": "Vergrößern",
|
||||||
|
"zoom_out": "Verkleinern",
|
||||||
|
"download": "Herunterladen",
|
||||||
|
"download_in_progress": "Herunterladen läuft",
|
||||||
|
"downloading": "Herunterladen",
|
||||||
|
"downloaded": "Bereits heruntergeladen",
|
||||||
|
"menu": "Menü",
|
||||||
|
},
|
||||||
|
"es": {
|
||||||
|
"idle": "Suelta archivos o URLs aquí para reproducirlos",
|
||||||
|
"na": "No disponible",
|
||||||
|
"video": "Video",
|
||||||
|
"audio": "Audio",
|
||||||
|
"subtitle": "Subtítulo",
|
||||||
|
"no_subs": "No hay subtítulos disponibles",
|
||||||
|
"no_audio": "No hay pistas de audio disponibles",
|
||||||
|
"playlist": "Lista de reproducción",
|
||||||
|
"no_playlist": "La lista de reproducción está vacía",
|
||||||
|
"chapter": "Capítulo",
|
||||||
|
"ontop": "Fijar ventana",
|
||||||
|
"ontop_disable": "Desfijar ventana",
|
||||||
|
"loop_enable": "Activar bucle",
|
||||||
|
"loop_disable": "Desactivar bucle",
|
||||||
|
"speed_control": "Control de velocidad",
|
||||||
|
"screenshot": "Captura de pantalla",
|
||||||
|
"stats_info": "Información",
|
||||||
|
"cache": "Caché",
|
||||||
|
"buffering": "Cargando",
|
||||||
|
"zoom_in": "Ampliar",
|
||||||
|
"zoom_out": "Reducir",
|
||||||
|
"download": "Descargar",
|
||||||
|
"download_in_progress": "Descarga en curso",
|
||||||
|
"downloading": "Descargando",
|
||||||
|
"downloaded": "Ya descargado",
|
||||||
|
"menu": "Menú",
|
||||||
|
},
|
||||||
|
"pl": {
|
||||||
|
"idle": "Upuść plik lub łącze URL do odtworzenia",
|
||||||
|
"na": "Niedostępne",
|
||||||
|
"video": "Wideo",
|
||||||
|
"audio": "Audio",
|
||||||
|
"subtitle": "Napisy",
|
||||||
|
"no_subs": "Brak dostępnych napisów",
|
||||||
|
"no_audio": "Brak dostępnych ścieżek dźwiękowych",
|
||||||
|
"playlist": "Lista odtwarzania",
|
||||||
|
"no_playlist": "Lista odtwarzania jest pusta",
|
||||||
|
"chapter": "Rozdział",
|
||||||
|
"ontop": "Przypnij okno",
|
||||||
|
"ontop_disable": "Odepnij okno",
|
||||||
|
"loop_enable": "Włącz zapętlenie",
|
||||||
|
"loop_disable": "Wyłącz zapętlenie",
|
||||||
|
"speed_control": "Kontrola prędkości",
|
||||||
|
"screenshot": "Zrzut ekranu",
|
||||||
|
"stats_info": "Informacja",
|
||||||
|
"cache": "Pamięć podręczna",
|
||||||
|
"buffering": "Buforowanie",
|
||||||
|
"zoom_in": "Powiększ",
|
||||||
|
"zoom_out": "Pomniejsz",
|
||||||
|
"download": "Pobierz",
|
||||||
|
"download_in_progress": "Pobieranie w toku",
|
||||||
|
"downloading": "Pobieranie",
|
||||||
|
"downloaded": "Już pobrano",
|
||||||
|
"menu": "Menu",
|
||||||
|
},
|
||||||
|
"jp": {
|
||||||
|
"idle": "ファイルやURLのリンクをここにドロップすると再生されます",
|
||||||
|
"na": "利用できません",
|
||||||
|
"video": "ビデオ",
|
||||||
|
"audio": "オーディオ",
|
||||||
|
"subtitle": "字幕",
|
||||||
|
"no_subs": "利用可能な字幕はありません",
|
||||||
|
"no_audio": "オーディオトラックはありません",
|
||||||
|
"playlist": "プレイリスト",
|
||||||
|
"no_playlist": "プレイリストは空です",
|
||||||
|
"chapter": "チャプター",
|
||||||
|
"ontop": "ウィンドウを最前面に固定",
|
||||||
|
"ontop_disable": "ウィンドウを固定解除",
|
||||||
|
"loop_enable": "ループを有効にする",
|
||||||
|
"loop_disable": "ループを無効にする",
|
||||||
|
"speed_control": "速度制御",
|
||||||
|
"screenshot": "スクリーンショット",
|
||||||
|
"stats_info": "情報",
|
||||||
|
"cache": "キャッシュ",
|
||||||
|
"buffering": "読み込み中",
|
||||||
|
"zoom_in": "拡大",
|
||||||
|
"zoom_out": "縮小",
|
||||||
|
"download": "ダウンロード",
|
||||||
|
"download_in_progress": "ダウンロード中",
|
||||||
|
"downloading": "ダウンロード中",
|
||||||
|
"downloaded": "既にダウンロード済み",
|
||||||
|
"menu": "メニュー",
|
||||||
|
},
|
||||||
|
"zh": {
|
||||||
|
"idle": "将文件或URL放在这里播放",
|
||||||
|
"na": "不可用",
|
||||||
|
"video": "视频",
|
||||||
|
"audio": "音频",
|
||||||
|
"subtitle": "字幕",
|
||||||
|
"no_subs": "没有可用字幕",
|
||||||
|
"no_audio": "没有音轨",
|
||||||
|
"playlist": "播放列表",
|
||||||
|
"no_playlist": "播放列表为空",
|
||||||
|
"chapter": "章节",
|
||||||
|
"ontop": "窗口停留在顶层",
|
||||||
|
"ontop_disable": "禁用窗口停留在顶层",
|
||||||
|
"loop_enable": "启用循环",
|
||||||
|
"loop_disable": "禁用循环",
|
||||||
|
"speed_control": "速度控制",
|
||||||
|
"screenshot": "截屏",
|
||||||
|
"stats_info": "信息",
|
||||||
|
"cache": "缓存",
|
||||||
|
"buffering": "缓冲中",
|
||||||
|
"zoom_in": "放大",
|
||||||
|
"zoom_out": "缩小",
|
||||||
|
"download": "下载",
|
||||||
|
"download_in_progress": "下载中",
|
||||||
|
"downloading": "正在下载",
|
||||||
|
"downloaded": "已下载",
|
||||||
|
"menu": "菜单",
|
||||||
|
},
|
||||||
|
"ar": {
|
||||||
|
"idle": "أسقط الملفات أو الروابط هنا للتشغيل",
|
||||||
|
"na": "غير متاح",
|
||||||
|
"video": "فيديو",
|
||||||
|
"audio": "صوت",
|
||||||
|
"subtitle": "ترجمة",
|
||||||
|
"no_subs": "لا توجد ترجمات متاحة",
|
||||||
|
"no_audio": "لا توجد مسارات صوتية متاحة",
|
||||||
|
"playlist": "قائمة التشغيل",
|
||||||
|
"no_playlist": "قائمة التشغيل فارغة",
|
||||||
|
"chapter": "فصل",
|
||||||
|
"ontop": "تثبيت النافذة",
|
||||||
|
"ontop_disable": "إلغاء تثبيت النافذة",
|
||||||
|
"loop_enable": "تمكين التكرار",
|
||||||
|
"loop_disable": "تعطيل التكرار",
|
||||||
|
"speed_control": "التحكم في السرعة",
|
||||||
|
"screenshot": "لقطة شاشة",
|
||||||
|
"stats_info": "معلومات",
|
||||||
|
"cache": "ذاكرة التخزين المؤقت",
|
||||||
|
"buffering": "جار التحميل",
|
||||||
|
"zoom_in": "تكبير",
|
||||||
|
"zoom_out": "تصغير",
|
||||||
|
"download": "تنزيل",
|
||||||
|
"download_in_progress": "التنزيل جارٍ",
|
||||||
|
"downloading": "جاري التنزيل",
|
||||||
|
"downloaded": "تم التنزيل بالفعل",
|
||||||
|
"menu": "قائمة",
|
||||||
|
},
|
||||||
|
"ru": {
|
||||||
|
"idle": "Перенести файл или URL для воспроизведения",
|
||||||
|
"na": "Недоступно",
|
||||||
|
"video": "Видео",
|
||||||
|
"audio": "Аудио",
|
||||||
|
"subtitle": "Субтитры",
|
||||||
|
"no_subs": "Субтитры недоступны",
|
||||||
|
"no_audio": "Нет аудио-дорожек",
|
||||||
|
"playlist": "Плейлист",
|
||||||
|
"no_playlist": "Плейлист пуст",
|
||||||
|
"chapter": "Глава",
|
||||||
|
"ontop": "Закрепить окно",
|
||||||
|
"ontop_disable": "Открепить окно",
|
||||||
|
"loop_enable": "Повтор",
|
||||||
|
"loop_disable": "Отключить повтор",
|
||||||
|
"speed_control": "Контроль скорости",
|
||||||
|
"screenshot": "Скриншот",
|
||||||
|
"stats_info": "Информация",
|
||||||
|
"cache": "Кеш",
|
||||||
|
"buffering": "Буферизация",
|
||||||
|
"zoom_in": "Зум +",
|
||||||
|
"zoom_out": "Зум -",
|
||||||
|
"download": "Загрузить",
|
||||||
|
"download_in_progress": "Загрузка в процессе",
|
||||||
|
"downloading": "Загрузка",
|
||||||
|
"downloaded": "Уже загружено",
|
||||||
|
"menu": "Меню",
|
||||||
|
},
|
||||||
|
}
|
||||||
68
submodules/ModernZ/extras/pause-indicator-lite/README.md
Normal file
68
submodules/ModernZ/extras/pause-indicator-lite/README.md
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
## Pause Indicator Lite
|
||||||
|
|  |  |
|
||||||
|
|:---:|:---:|
|
||||||
|
|
||||||
|
A simple script that displays an indicator on pause (and mute), with options to adjust icon type, color, height, width, opacity and whether to toggle pause with a keybind or not.
|
||||||
|
|
||||||
|
I only decided to write this because the ones I found were either too complicated or too simple. The alternatives are great, this one just meets my simple use case scenario.
|
||||||
|
|
||||||
|
**Script:** [Pause Indicator Lite](./pause_indicator_lite.lua)
|
||||||
|
|
||||||
|
### Indicator Options
|
||||||
|
Below is the full list for indicator options and their default values.
|
||||||
|
|
||||||
|
To adjust them you can either:
|
||||||
|
- Place [pause_indicator_lite.conf](./pause_indicator_lite.conf) in `script-opts` and change the values in it [recommended]
|
||||||
|
- Simply change their values in `local options` within the script itself
|
||||||
|
|
||||||
|
| Option | Value | Description |
|
||||||
|
|--------------------------|-------------|--------------------------------------------------------------------------------------------------------------------------------------|
|
||||||
|
| `indicator_icon` | pause | indicator icon type. `pause`, `play` |
|
||||||
|
| `indicator_stay` | yes | keep indicator visibile during pause |
|
||||||
|
| `indicator_timeout` | 0.6 | timeout (seconds) if indicator doesn't stay |
|
||||||
|
| `keybind_allow` | yes | allow keybind to toggle pause |
|
||||||
|
| `keybind_set` | mbtn_left | the used keybind to toggle pause [[reference](https://github.com/mpv-player/mpv/blob/master/etc/input.conf)] |
|
||||||
|
| `keybind_mode` | onpause | mode to activate keybind. <br>`onpause`: only active when paused, to unpause <br>`always`: always active to toggle pause/unpause |
|
||||||
|
| `keybind_eof_disable` | yes | disable keybind on eof (end of file) [[reference](https://github.com/Samillion/ModernZ/issues/291)] |
|
||||||
|
| `icon_color` | `#FFFFFF` | icon fill color |
|
||||||
|
| `icon_border_color` | `#111111` | icon border color |
|
||||||
|
| `icon_border_width` | 1.5 | icon border width |
|
||||||
|
| `icon_opacity` | 40 | icon opacity (0-100) |
|
||||||
|
| `rectangles_width` | 30 | width of rectangles (pause icon) |
|
||||||
|
| `rectangles_height` | 80 | height of rectangles (pause icon) |
|
||||||
|
| `rectangles_spacing` | 20 | spacing between the two rectangles (pause icon) |
|
||||||
|
| `triangle_width` | 80 | width of triangle (play icon) |
|
||||||
|
| `triangle_height` | 80 | height of triangle (play icon) |
|
||||||
|
| `flash_play_icon` | yes | flash play icon on unpause (best with pause indicator icon) |
|
||||||
|
| `flash_icon_timeout` | 0.3 | timeout (seconds) for flash icon |
|
||||||
|
| `fluent_icons` | no | requires `fonts/fluent-system-icons.ttf` [[details](https://github.com/Samillion/ModernZ/pull/336)] |
|
||||||
|
| `fluent_icon_size` | 80 | fluent icon size |
|
||||||
|
| `mute_indicator` | no | show a mute indicator (requires fluent font) |
|
||||||
|
| `mute_indicator_pos` | middle_right | position of mute indicator. `top_left`, `top_right`, `top_center`. also: `middle_*`, `bottom_*` same as `top_*` (ie: `bottom_right`) |
|
||||||
|
|
||||||
|
### How to install
|
||||||
|
|
||||||
|
Simply place `pause_indicator_lite.lua` in the corresponding mpv scripts folder of your operating system:
|
||||||
|
|
||||||
|
- Windows: `%APPDATA%\mpv\scripts\` or `C:\users\USERNAME\AppData\Roaming\mpv\scripts\`
|
||||||
|
- Linux: `~/.config/mpv/scripts/` or `/home/USERNAME/.config/mpv/scripts/`
|
||||||
|
- Mac: `~/.config/mpv/scripts/` or `/Users/USERNAME/.config/mpv/scripts/`
|
||||||
|
|
||||||
|
> [!TIP]
|
||||||
|
> More information about mpv files locations can be found [here](https://mpv.io/manual/master/#files)
|
||||||
|
|
||||||
|
```
|
||||||
|
mpv
|
||||||
|
├── fonts/
|
||||||
|
│ └── fluent-system-icons.ttf (optional) [required for fluent_icons]
|
||||||
|
├── script-opts
|
||||||
|
│ └── pause_indicator_lite.conf
|
||||||
|
└── scripts
|
||||||
|
└── pause_indicator_lite.lua
|
||||||
|
```
|
||||||
|
|
||||||
|
### Notes
|
||||||
|
|
||||||
|
- I don't plan to focus many updates on this script, because ASS drawing and positioning pisses me off. 😝
|
||||||
|
- Other alternatives can be found in the mpv [user scripts](https://github.com/mpv-player/mpv/wiki/User-Scripts) wiki.
|
||||||
|
- This isn't exclusive to be used on ModernZ OSC. Feel free to use it with whatever you want.
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
# indicator icon type. "pause", "play"
|
||||||
|
indicator_icon=pause
|
||||||
|
# keep indicator visibile during pause
|
||||||
|
indicator_stay=yes
|
||||||
|
# timeout (seconds) if indicator doesn't stay
|
||||||
|
indicator_timeout=0.6
|
||||||
|
|
||||||
|
# allow keybind to toggle pause
|
||||||
|
keybind_allow=yes
|
||||||
|
# the used keybind to toggle pause
|
||||||
|
keybind_set=mbtn_left
|
||||||
|
# mode to activate keybind. "onpause", "always"
|
||||||
|
keybind_mode=onpause
|
||||||
|
# disable keybind on eof (end of file)
|
||||||
|
keybind_eof_disable=yes
|
||||||
|
|
||||||
|
# icon fill color
|
||||||
|
icon_color=#FFFFFF
|
||||||
|
# icon border color
|
||||||
|
icon_border_color=#111111
|
||||||
|
# icon border width
|
||||||
|
icon_border_width=1.5
|
||||||
|
# icon opacity (0-100)
|
||||||
|
icon_opacity=40
|
||||||
|
|
||||||
|
# width of rectangles
|
||||||
|
rectangles_width=30
|
||||||
|
# height of rectangles
|
||||||
|
rectangles_height=80
|
||||||
|
# spacing between the two rectangles
|
||||||
|
rectangles_spacing=20
|
||||||
|
|
||||||
|
# width of triangle
|
||||||
|
triangle_width=80
|
||||||
|
# height of triangle
|
||||||
|
triangle_height=80
|
||||||
|
|
||||||
|
# flash play icon on unpause
|
||||||
|
flash_play_icon=yes
|
||||||
|
# timeout (seconds) for flash icon
|
||||||
|
flash_icon_timeout=0.3
|
||||||
|
|
||||||
|
# icon style used in ModernZ osc
|
||||||
|
# requires fonts/fluent-system-icons.ttf
|
||||||
|
fluent_icons=no
|
||||||
|
# fluent icon size
|
||||||
|
fluent_icon_size=80
|
||||||
|
|
||||||
|
# mute options
|
||||||
|
# show a mute indicator (requires fluent font)
|
||||||
|
mute_indicator=no
|
||||||
|
# position of mute indicator. top_left, top_right, top_center
|
||||||
|
# also: middle_*, bottom_* same as top_* (ie: bottom_right)
|
||||||
|
mute_indicator_pos=middle_right
|
||||||
@@ -0,0 +1,218 @@
|
|||||||
|
--[[
|
||||||
|
|
||||||
|
A simple script that shows a pause indicator, on pause
|
||||||
|
https://github.com/Samillion/ModernZ/tree/main/extras/pause-indicator-lite
|
||||||
|
|
||||||
|
--]]
|
||||||
|
|
||||||
|
local options = {
|
||||||
|
-- indicator icon type
|
||||||
|
indicator_icon = "pause", -- indicator icon type. "pause", "play"
|
||||||
|
indicator_stay = true, -- keep indicator visibile during pause
|
||||||
|
indicator_timeout = 0.6, -- timeout (seconds) if indicator doesn't stay
|
||||||
|
|
||||||
|
-- keybind
|
||||||
|
keybind_allow = true, -- allow keybind to toggle pause
|
||||||
|
keybind_set = "mbtn_left", -- the used keybind to toggle pause
|
||||||
|
keybind_mode = "onpause", -- mode to activate keybind. "onpause", "always"
|
||||||
|
keybind_eof_disable = true, -- disable keybind on eof (end of file)
|
||||||
|
|
||||||
|
-- icon colors & opacity
|
||||||
|
icon_color = "#FFFFFF", -- icon fill color
|
||||||
|
icon_border_color = "#111111", -- icon border color
|
||||||
|
icon_border_width = 1.5, -- icon border width
|
||||||
|
icon_opacity = 40, -- icon opacity (0-100)
|
||||||
|
|
||||||
|
-- pause icon
|
||||||
|
rectangles_width = 30, -- width of rectangles
|
||||||
|
rectangles_height = 80, -- height of rectangles
|
||||||
|
rectangles_spacing = 20, -- spacing between the two rectangles
|
||||||
|
|
||||||
|
-- play icon
|
||||||
|
triangle_width = 80, -- width of triangle
|
||||||
|
triangle_height = 80, -- height of triangle
|
||||||
|
|
||||||
|
-- best with pause icon
|
||||||
|
flash_play_icon = true, -- flash play icon on unpause
|
||||||
|
flash_icon_timeout = 0.3, -- timeout (seconds) for flash icon
|
||||||
|
|
||||||
|
-- icon style used in ModernZ osc
|
||||||
|
fluent_icons = false, -- requires fonts/fluent-system-icons.ttf
|
||||||
|
fluent_icon_size = 80, -- fluent icon size
|
||||||
|
|
||||||
|
-- mute options
|
||||||
|
mute_indicator = false, -- show a mute indicator (requires fluent font)
|
||||||
|
mute_indicator_pos = "middle_right", -- position of mute indicator. top_left, top_right, top_center
|
||||||
|
-- also: middle_*, bottom_* same as top_* (ie: bottom_right)
|
||||||
|
}
|
||||||
|
|
||||||
|
local msg = require "mp.msg"
|
||||||
|
require 'mp.options'.read_options(options, "pause_indicator_lite")
|
||||||
|
|
||||||
|
-- convert color from hex (adjusted from mpv/osc.lua)
|
||||||
|
local function convert_color(color)
|
||||||
|
if color:find("^#%x%x%x%x%x%x$") == nil then
|
||||||
|
msg.warn("'" .. color .. "' is not a valid color, using default '#FFFFFF'")
|
||||||
|
return "FFFFFF" -- color fallback
|
||||||
|
end
|
||||||
|
return color:sub(6,7) .. color:sub(4,5) .. color:sub(2,3)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- convert percentage opacity (0-100) to ASS alpha values
|
||||||
|
local function convert_opacity(value)
|
||||||
|
value = math.max(0, math.min(100, value))
|
||||||
|
return string.format("%02X", (255 - (value * 2.55)))
|
||||||
|
end
|
||||||
|
|
||||||
|
-- colors and opaicty
|
||||||
|
local icon_color = convert_color(options.icon_color)
|
||||||
|
local icon_border_color = convert_color(options.icon_border_color)
|
||||||
|
local icon_opacity = convert_opacity(options.icon_opacity)
|
||||||
|
local icon_font = "fluent-system-icons"
|
||||||
|
|
||||||
|
-- pause icon
|
||||||
|
local function draw_rectangles()
|
||||||
|
if options.fluent_icons then
|
||||||
|
local pause_icon = "\238\163\140"
|
||||||
|
return string.format([[{\\rDefault\\an5\\alpha&H%s\\bord%s\\1c&H%s&\\3c&H%s&\\fs%s\\fn%s}%s]],
|
||||||
|
icon_opacity, options.icon_border_width, icon_color, icon_border_color, options.fluent_icon_size, icon_font, pause_icon)
|
||||||
|
else
|
||||||
|
return string.format([[{\\rDefault\\p1\\an5\\alpha&H%s\\bord%s\\1c&H%s&\\3c&H%s&}m 0 0 l %d 0 l %d %d l 0 %d m %d 0 l %d 0 l %d %d l %d %d{\\p0}]],
|
||||||
|
icon_opacity, options.icon_border_width, icon_color, icon_border_color, options.rectangles_width, options.rectangles_width,
|
||||||
|
options.rectangles_height, options.rectangles_height, options.rectangles_width + options.rectangles_spacing,
|
||||||
|
options.rectangles_width * 2 + options.rectangles_spacing, options.rectangles_width * 2 + options.rectangles_spacing,
|
||||||
|
options.rectangles_height, options.rectangles_width + options.rectangles_spacing, options.rectangles_height)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- play icon
|
||||||
|
local function draw_triangle()
|
||||||
|
if options.fluent_icons then
|
||||||
|
local play_icon = "\238\166\143"
|
||||||
|
return string.format([[{\\rDefault\\an5\\alpha&H%s\\bord%s\\1c&H%s&\\3c&H%s&\\fs%s\\fn%s}%s]],
|
||||||
|
icon_opacity, options.icon_border_width, icon_color, icon_border_color, options.fluent_icon_size, icon_font, play_icon)
|
||||||
|
else
|
||||||
|
return string.format([[{\\rDefault\\p1\\an5\\alpha&H%s\\bord%s\\1c&H%s&\\3c&H%s&}m 0 0 l %d %d l 0 %d{\\p0}]],
|
||||||
|
icon_opacity, options.icon_border_width, icon_color, icon_border_color, options.triangle_width, options.triangle_height / 2, options.triangle_height)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- mute icon
|
||||||
|
local function draw_mute()
|
||||||
|
if not options.fluent_icons then return end
|
||||||
|
|
||||||
|
local mute_icon = "\238\173\138"
|
||||||
|
local mute_pos_list = {
|
||||||
|
["top_left"] = 7,
|
||||||
|
["top_center"] = 8,
|
||||||
|
["top_right"] = 9,
|
||||||
|
["middle_left"] = 4,
|
||||||
|
["middle_center"] = 5,
|
||||||
|
["middle_right"] = 6,
|
||||||
|
["bottom_left"] = 1,
|
||||||
|
["bottom_center"] = 2,
|
||||||
|
["bottom_right"] = 3,
|
||||||
|
}
|
||||||
|
local mute_pos = mute_pos_list[options.mute_indicator_pos:lower()] or 6
|
||||||
|
return string.format([[{\\rDefault\\an%s\\alpha&H%s\\bord%s\\1c&H%s&\\3c&H%s&\\fs%s\\fn%s}%s]],
|
||||||
|
mute_pos, icon_opacity, options.icon_border_width, icon_color, icon_border_color, options.fluent_icon_size, icon_font, mute_icon)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- initiate overlay
|
||||||
|
local indicator = mp.create_osd_overlay("ass-events")
|
||||||
|
local flash = mp.create_osd_overlay("ass-events")
|
||||||
|
local mute = mp.create_osd_overlay("ass-events")
|
||||||
|
|
||||||
|
-- keep track of pause toggle and end of file
|
||||||
|
local toggled, eof
|
||||||
|
|
||||||
|
-- draw and update indicator
|
||||||
|
local function update_indicator()
|
||||||
|
local _, _, display_aspect = mp.get_osd_size()
|
||||||
|
if display_aspect == 0 or (indicator.visible and not toggled) then return end
|
||||||
|
|
||||||
|
indicator.data = options.indicator_icon == "play" and draw_triangle() or draw_rectangles()
|
||||||
|
indicator:update()
|
||||||
|
|
||||||
|
if not options.indicator_stay then
|
||||||
|
mp.add_timeout(options.indicator_timeout, function() indicator:remove() end)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- flash play icon
|
||||||
|
local function flash_icon()
|
||||||
|
if not options.flash_play_icon then return flash:remove() end
|
||||||
|
flash.data = draw_triangle()
|
||||||
|
flash:update()
|
||||||
|
mp.add_timeout(options.flash_icon_timeout, function() flash:remove() end)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- draw mute icon
|
||||||
|
local function mute_icon()
|
||||||
|
mute.data = draw_mute()
|
||||||
|
mute:update()
|
||||||
|
end
|
||||||
|
|
||||||
|
-- check if file is video
|
||||||
|
local function is_video()
|
||||||
|
local t = mp.get_property_native("current-tracks/video")
|
||||||
|
return t and not (t.image or t.albumart) and true or false
|
||||||
|
end
|
||||||
|
|
||||||
|
-- remove overlays
|
||||||
|
local function shutdown()
|
||||||
|
if flash then flash:remove() end
|
||||||
|
if indicator then indicator:remove() end
|
||||||
|
mp.unobserve_property("pause")
|
||||||
|
end
|
||||||
|
|
||||||
|
-- end of file keybind check
|
||||||
|
if options.keybind_eof_disable then
|
||||||
|
mp.observe_property("eof-reached", "bool", function(_, val)
|
||||||
|
eof = val
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- observe when pause state changes
|
||||||
|
mp.observe_property("pause", "bool", function(_, paused)
|
||||||
|
if not is_video() then return shutdown() end
|
||||||
|
if paused then
|
||||||
|
update_indicator()
|
||||||
|
toggled = true
|
||||||
|
if options.flash_play_icon then flash:remove() end
|
||||||
|
else
|
||||||
|
indicator:remove()
|
||||||
|
if toggled then
|
||||||
|
flash_icon()
|
||||||
|
toggled = false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- keybind setup (if options allow it)
|
||||||
|
if options.keybind_allow == true then
|
||||||
|
mp.set_key_bindings({
|
||||||
|
{options.keybind_set, function() mp.commandv("cycle", "pause") end}
|
||||||
|
}, "pause-indicator", "force")
|
||||||
|
|
||||||
|
if options.keybind_mode == "always" or (options.keybind_mode == "onpause" and paused) then
|
||||||
|
if not eof then mp.enable_key_bindings("pause-indicator") end
|
||||||
|
else
|
||||||
|
mp.disable_key_bindings("pause-indicator")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
|
||||||
|
-- update pause indicator position if window size changes
|
||||||
|
mp.observe_property("osd-dimensions", "native", function()
|
||||||
|
if indicator and indicator.visible then
|
||||||
|
update_indicator()
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
|
||||||
|
if options.mute_indicator and options.fluent_icons then
|
||||||
|
mp.observe_property("mute", "bool", function(_, val)
|
||||||
|
if val and not mute.visible then mute_icon() else mute:remove() end
|
||||||
|
end)
|
||||||
|
else
|
||||||
|
mute:remove()
|
||||||
|
end
|
||||||
BIN
submodules/ModernZ/fluent-system-icons.ttf
Normal file
BIN
submodules/ModernZ/fluent-system-icons.ttf
Normal file
Binary file not shown.
BIN
submodules/ModernZ/material-design-icons.ttf
Normal file
BIN
submodules/ModernZ/material-design-icons.ttf
Normal file
Binary file not shown.
384
submodules/ModernZ/modernz.conf
Normal file
384
submodules/ModernZ/modernz.conf
Normal file
@@ -0,0 +1,384 @@
|
|||||||
|
# Language and display
|
||||||
|
# set language (for available options, see: https://github.com/Samillion/ModernZ/blob/main/docs/TRANSLATIONS.md)
|
||||||
|
language=en
|
||||||
|
# set icon theme. accepts fluent or material
|
||||||
|
icon_theme=fluent
|
||||||
|
# font for the OSC (default: mpv-osd-symbols or the one set in mpv.conf)
|
||||||
|
font=mpv-osd-symbols
|
||||||
|
|
||||||
|
# show mpv logo when idle
|
||||||
|
idlescreen=yes
|
||||||
|
# show OSC window top bar: "auto", "yes", or "no" (borderless/fullscreen)
|
||||||
|
window_top_bar=auto
|
||||||
|
# show OSC when windowed
|
||||||
|
showwindowed=yes
|
||||||
|
# show OSC when fullscreen
|
||||||
|
showfullscreen=yes
|
||||||
|
# show OSC when paused
|
||||||
|
showonpause=yes
|
||||||
|
# disable OSC hide timeout when paused
|
||||||
|
keeponpause=yes
|
||||||
|
# disable Santa hat in December
|
||||||
|
greenandgrumpy=no
|
||||||
|
|
||||||
|
# OSC behaviour and scaling
|
||||||
|
# time (in ms) before OSC hides if no mouse movement
|
||||||
|
hidetimeout=1500
|
||||||
|
# if seeking should reset the hidetimeout
|
||||||
|
seek_resets_hidetimeout=yes
|
||||||
|
# fade-out duration (in ms), set to 0 for no fade
|
||||||
|
fadeduration=200
|
||||||
|
# whether to enable fade-in effect
|
||||||
|
fadein=no
|
||||||
|
# minimum mouse movement (in pixels) required to show OSC
|
||||||
|
minmousemove=0
|
||||||
|
# show OSC only when hovering at the bottom
|
||||||
|
bottomhover=yes
|
||||||
|
# height of hover zone for bottomhover (in pixels)
|
||||||
|
bottomhover_zone=130
|
||||||
|
# show OSC when seeking
|
||||||
|
osc_on_seek=no
|
||||||
|
# show OSC on start of every file
|
||||||
|
osc_on_start=no
|
||||||
|
# pause video while seeking with mouse move (on button hold)
|
||||||
|
mouse_seek_pause=yes
|
||||||
|
# force show seekbar tooltip on mouse drag, even if not hovering seekbar
|
||||||
|
force_seek_tooltip=no
|
||||||
|
|
||||||
|
# scale osc with the video
|
||||||
|
vidscale=auto
|
||||||
|
# osc scale factor when windowed
|
||||||
|
scalewindowed=1.0
|
||||||
|
# osc scale factor when fullscreen
|
||||||
|
scalefullscreen=1.0
|
||||||
|
|
||||||
|
# Elements display
|
||||||
|
# show title in the OSC (above seekbar)
|
||||||
|
show_title=yes
|
||||||
|
# title above seekbar format: "${media-title}" or "${filename}"
|
||||||
|
title=${media-title}
|
||||||
|
# font size of the title text (above seekbar)
|
||||||
|
title_font_size=24
|
||||||
|
# chapter title font size
|
||||||
|
chapter_title_font_size=14
|
||||||
|
# show cached time information
|
||||||
|
cache_info=no
|
||||||
|
# show cache speed per second
|
||||||
|
cache_info_speed=no
|
||||||
|
# font size of the cache information
|
||||||
|
cache_info_font_size=12
|
||||||
|
|
||||||
|
# show chapter title alongside timestamp (below seekbar)
|
||||||
|
show_chapter_title=yes
|
||||||
|
# format for chapter display on seekbar hover (set to "no" to disable)
|
||||||
|
chapter_fmt=%s
|
||||||
|
|
||||||
|
# show total time instead of remaining time
|
||||||
|
timetotal=yes
|
||||||
|
# show timecodes with milliseconds
|
||||||
|
timems=no
|
||||||
|
# use the Unicode minus sign in remaining time
|
||||||
|
unicodeminus=no
|
||||||
|
# "dynamic" or "fixed". dynamic shows MM:SS when possible, fixed always shows HH:MM:SS
|
||||||
|
time_format=dynamic
|
||||||
|
# font size of the time display
|
||||||
|
time_font_size=16
|
||||||
|
# tooltips font size
|
||||||
|
tooltip_font_size=14
|
||||||
|
|
||||||
|
# Title bar settings
|
||||||
|
# show window title in borderless/fullscreen mode
|
||||||
|
window_title=no
|
||||||
|
# show window controls (close, minimize, maximize) in borderless/fullscreen
|
||||||
|
window_controls=yes
|
||||||
|
# same as title but for window_top_bar
|
||||||
|
windowcontrols_title=${media-title}
|
||||||
|
|
||||||
|
# Subtitle display settings
|
||||||
|
# raise subtitles above the OSC when shown
|
||||||
|
raise_subtitles=yes
|
||||||
|
# amount by which subtitles are raised when the OSC is shown (in pixels)
|
||||||
|
raise_subtitle_amount=125
|
||||||
|
|
||||||
|
# Buttons display and functionality
|
||||||
|
# show the jump backward and forward buttons
|
||||||
|
jump_buttons=yes
|
||||||
|
# change the jump amount in seconds
|
||||||
|
jump_amount=10
|
||||||
|
# change the jump amount in seconds when right-clicking jump buttons and shift-clicking chapter skip buttons
|
||||||
|
jump_more_amount=60
|
||||||
|
# show different icon when jump_amount is set to 5, 10, or 30
|
||||||
|
jump_icon_number=yes
|
||||||
|
# seek mode for jump buttons
|
||||||
|
jump_mode=relative
|
||||||
|
# enable continuous jumping when holding down seek buttons
|
||||||
|
jump_softrepeat=yes
|
||||||
|
# show the chapter skip backward and forward buttons
|
||||||
|
chapter_skip_buttons=no
|
||||||
|
# enable continuous skipping when holding down chapter skip buttons
|
||||||
|
chapter_softrepeat=yes
|
||||||
|
# show next/previous playlist track buttons
|
||||||
|
track_nextprev_buttons=yes
|
||||||
|
|
||||||
|
# show mute button and volume slider
|
||||||
|
volume_control=yes
|
||||||
|
# volume scale type: "linear" or "logarithmic"
|
||||||
|
volume_control_type=linear
|
||||||
|
# show playlist button: Left-click for simple playlist, Right-click for interactive playlist
|
||||||
|
playlist_button=yes
|
||||||
|
# hide playlist button when no playlist exists
|
||||||
|
hide_empty_playlist_button=no
|
||||||
|
# gray out the playlist button when no playlist exists
|
||||||
|
gray_empty_playlist_button=no
|
||||||
|
|
||||||
|
# show download button on web videos (requires yt-dlp and ffmpeg)
|
||||||
|
download_button=yes
|
||||||
|
# default download directory for videos (https://mpv.io/manual/master/#paths)
|
||||||
|
download_path=~~desktop/mpv
|
||||||
|
# show screenshot button
|
||||||
|
screenshot_button=no
|
||||||
|
# flag for screenshot button: "subtitles", "video", "window", "each-frame"
|
||||||
|
screenshot_flag=subtitles
|
||||||
|
|
||||||
|
# show window on top button
|
||||||
|
ontop_button=yes
|
||||||
|
# show loop button
|
||||||
|
loop_button=no
|
||||||
|
# show speed control button
|
||||||
|
speed_button=no
|
||||||
|
# speed change amount per click
|
||||||
|
speed_button_click=1
|
||||||
|
# speed change amount on scroll
|
||||||
|
speed_button_scroll=0.25
|
||||||
|
# show info button
|
||||||
|
info_button=yes
|
||||||
|
# show fullscreen toggle button
|
||||||
|
fullscreen_button=yes
|
||||||
|
|
||||||
|
# enable looping by right-clicking pause
|
||||||
|
loop_in_pause=yes
|
||||||
|
|
||||||
|
# force buttons to always be active. can add: playlist_prev,playlist_next
|
||||||
|
buttons_always_active=none
|
||||||
|
|
||||||
|
# icon size for the play/pause button
|
||||||
|
playpause_size=28
|
||||||
|
# icon size for the middle buttons
|
||||||
|
midbuttons_size=24
|
||||||
|
# icon size for the side buttons
|
||||||
|
sidebuttons_size=24
|
||||||
|
|
||||||
|
# show zoom controls in image viewer mode
|
||||||
|
zoom_control=yes
|
||||||
|
# maximum zoom in value
|
||||||
|
zoom_in_max=4
|
||||||
|
# minimum zoom out value
|
||||||
|
zoom_out_min=-1
|
||||||
|
|
||||||
|
# Colors and style
|
||||||
|
# accent color of the OSC and title bar
|
||||||
|
osc_color=#000000
|
||||||
|
# color of the title in borderless/fullscreen mode
|
||||||
|
window_title_color=#FFFFFF
|
||||||
|
# color of the window controls (close, minimize, maximize) in borderless/fullscreen mode
|
||||||
|
window_controls_color=#FFFFFF
|
||||||
|
# color of close window control on hover
|
||||||
|
windowcontrols_close_hover=#F45C5B
|
||||||
|
# color of maximize window controls on hover
|
||||||
|
windowcontrols_max_hover=#F8BC3A
|
||||||
|
# color of minimize window controls on hover
|
||||||
|
windowcontrols_min_hover=#43CB44
|
||||||
|
# color of the title (above seekbar)
|
||||||
|
title_color=#FFFFFF
|
||||||
|
# color of the cache information
|
||||||
|
cache_info_color=#FFFFFF
|
||||||
|
# color of the seekbar progress and handle
|
||||||
|
seekbarfg_color=#FB8C00
|
||||||
|
# color of the remaining seekbar
|
||||||
|
seekbarbg_color=#94754F
|
||||||
|
# color of the cache ranges on the seekbar
|
||||||
|
seekbar_cache_color=#918F8E
|
||||||
|
# match volume bar color with seekbar color (ignores side_buttons_color)
|
||||||
|
volumebar_match_seek_color=no
|
||||||
|
# color of the timestamps (below seekbar)
|
||||||
|
time_color=#FFFFFF
|
||||||
|
# color of the chapter title next to timestamp (below seekbar)
|
||||||
|
chapter_title_color=#FFFFFF
|
||||||
|
# color of the side buttons (audio, subtitles, playlist, etc.)
|
||||||
|
side_buttons_color=#FFFFFF
|
||||||
|
# color of the middle buttons (skip, jump, chapter, etc.)
|
||||||
|
middle_buttons_color=#FFFFFF
|
||||||
|
# color of the play/pause button
|
||||||
|
playpause_color=#FFFFFF
|
||||||
|
# color of the element when held down (pressed)
|
||||||
|
held_element_color=#999999
|
||||||
|
# color of a hovered button when hover_effect includes "color"
|
||||||
|
hover_effect_color=#FB8C00
|
||||||
|
# color of the border for thumbnails (with thumbfast)
|
||||||
|
thumbnail_border_color=#111111
|
||||||
|
# color of the border outline for thumbnails
|
||||||
|
thumbnail_border_outline=#404040
|
||||||
|
|
||||||
|
# alpha of the OSC background box
|
||||||
|
fade_alpha=130
|
||||||
|
# blur strength for the OSC alpha fade. caution: high values can take a lot of CPU time to render
|
||||||
|
fade_blur_strength=100
|
||||||
|
# use with "fade_blur_strength=0" to create a transparency box
|
||||||
|
fade_transparency_strength=0
|
||||||
|
# alpha of the window title bar (0 to disable)
|
||||||
|
window_fade_alpha=100
|
||||||
|
# blur strength for the window title bar. caution: high values can take a lot of CPU time to render
|
||||||
|
window_fade_blur_strength=100
|
||||||
|
# use with "window_fade_blur_strength=0" to create a transparency box
|
||||||
|
window_fade_transparency_strength=0
|
||||||
|
# width of the thumbnail border (for thumbfast)
|
||||||
|
thumbnail_border=3
|
||||||
|
# rounded corner radius for thumbnail border (0 to disable)
|
||||||
|
thumbnail_border_radius=3
|
||||||
|
|
||||||
|
# Button hover effects
|
||||||
|
# active button hover effects: "glow", "size", "color"; can use multiple separated by commas
|
||||||
|
hover_effect=size,glow,color
|
||||||
|
# relative size of a hovered button if "size" effect is active
|
||||||
|
hover_button_size=115
|
||||||
|
# glow intensity when "glow" hover effect is active
|
||||||
|
button_glow_amount=5
|
||||||
|
# apply hover size effect to slider handle
|
||||||
|
hover_effect_for_sliders=yes
|
||||||
|
|
||||||
|
# Tooltips and hints
|
||||||
|
# enable tooltips for disabled buttons and elements
|
||||||
|
tooltips_for_disabled_elements=yes
|
||||||
|
# enable text hints for info, loop, ontop, and screenshot buttons
|
||||||
|
tooltip_hints=yes
|
||||||
|
|
||||||
|
# Progress bar settings
|
||||||
|
# size ratio of the seekbar handle (range: 0 ~ 1)
|
||||||
|
seek_handle_size=0.8
|
||||||
|
# show seek range overlay
|
||||||
|
seekrange=yes
|
||||||
|
# transparency of the seek range
|
||||||
|
seekrangealpha=150
|
||||||
|
# update chapter markers on the seekbar when duration changes
|
||||||
|
livemarkers=yes
|
||||||
|
# use keyframes when dragging the seekbar
|
||||||
|
seekbarkeyframes=no
|
||||||
|
# top chapter nibbles above seekbar
|
||||||
|
nibbles_top=yes
|
||||||
|
# bottom chapter nibbles below seekbar
|
||||||
|
nibbles_bottom=yes
|
||||||
|
# chapter nibble style. "triangle", "bar" or "single-bar"
|
||||||
|
nibbles_style=triangle
|
||||||
|
|
||||||
|
# automatically set keyframes for the seekbar based on video length
|
||||||
|
automatickeyframemode=yes
|
||||||
|
# videos longer than this (in seconds) will have keyframes on the seekbar
|
||||||
|
automatickeyframelimit=600
|
||||||
|
|
||||||
|
# always show a small progress line at the bottom of the screen
|
||||||
|
persistentprogress=no
|
||||||
|
# height of the persistent progress bar
|
||||||
|
persistentprogressheight=17
|
||||||
|
# show buffer status on web videos in the persistent progress line
|
||||||
|
persistentbuffer=no
|
||||||
|
|
||||||
|
# Miscellaneous settings
|
||||||
|
# only used at init to set visibility_mode(...)
|
||||||
|
visibility=auto
|
||||||
|
# visibility modes to cycle through, modes are separated by _
|
||||||
|
visibility_modes=never_auto_always
|
||||||
|
# minimum interval between OSC redraws (in seconds)
|
||||||
|
tick_delay=0.03
|
||||||
|
# use display FPS as the minimum redraw interval
|
||||||
|
tick_delay_follow_display_fps=no
|
||||||
|
|
||||||
|
# Elements Position
|
||||||
|
# Useful when adjusting font size or type
|
||||||
|
|
||||||
|
# title height position above seekbar
|
||||||
|
title_height=96
|
||||||
|
# title height position if a chapter title is below it
|
||||||
|
title_with_chapter_height=108
|
||||||
|
# chapter title height position above seekbar
|
||||||
|
chapter_title_height=91
|
||||||
|
# time codes height position
|
||||||
|
time_codes_height=35
|
||||||
|
# time codes height position with portrait window
|
||||||
|
time_codes_centered_height=57
|
||||||
|
# tooltip height position offset
|
||||||
|
tooltip_height_offset=2
|
||||||
|
# if tooltip contains many characters, it is moved to the left by offset
|
||||||
|
tooltip_left_offset=5
|
||||||
|
# portrait window width trigger to move some elements
|
||||||
|
portrait_window_trigger=1000
|
||||||
|
# hide volume bar trigger window width
|
||||||
|
hide_volume_bar_trigger=1150
|
||||||
|
# osc height offset if title above seekbar is disabled
|
||||||
|
notitle_osc_h_offset=25
|
||||||
|
# osc height offset if chapter title is disabled or doesn't exist
|
||||||
|
nochapter_osc_h_offset=10
|
||||||
|
# seek hover timecodes tooltip height position offset
|
||||||
|
seek_hover_tooltip_h_offset=0
|
||||||
|
# osc height without offsets
|
||||||
|
osc_height=132
|
||||||
|
|
||||||
|
## Mouse commands
|
||||||
|
## details: https://github.com/Samillion/ModernZ#mouse-commands-user-options
|
||||||
|
|
||||||
|
# title above seekbar mouse actions
|
||||||
|
title_mbtn_left_command=script-binding stats/display-page-5
|
||||||
|
title_mbtn_mid_command=show-text ${path}
|
||||||
|
title_mbtn_right_command=script-binding select/select-watch-history; script-message-to modernz osc-hide
|
||||||
|
|
||||||
|
# playlist button mouse actions
|
||||||
|
playlist_mbtn_left_command=script-binding select/menu; script-message-to modernz osc-hide
|
||||||
|
playlist_mbtn_right_command=script-binding select/select-playlist; script-message-to modernz osc-hide
|
||||||
|
|
||||||
|
# volume mouse actions
|
||||||
|
vol_ctrl_mbtn_left_command=no-osd cycle mute
|
||||||
|
vol_ctrl_mbtn_right_command=script-binding select/select-audio-device; script-message-to modernz osc-hide
|
||||||
|
vol_ctrl_wheel_down_command=no-osd add volume -5
|
||||||
|
vol_ctrl_wheel_up_command=no-osd add volume 5
|
||||||
|
|
||||||
|
# audio button mouse actions
|
||||||
|
audio_track_mbtn_left_command=script-binding select/select-aid; script-message-to modernz osc-hide
|
||||||
|
audio_track_mbtn_mid_command=cycle audio down
|
||||||
|
audio_track_mbtn_right_command=cycle audio
|
||||||
|
audio_track_wheel_down_command=cycle audio
|
||||||
|
audio_track_wheel_up_command=cycle audio down
|
||||||
|
|
||||||
|
# subtitle button mouse actions
|
||||||
|
sub_track_mbtn_left_command=script-binding select/select-sid; script-message-to modernz osc-hide
|
||||||
|
sub_track_mbtn_mid_command=cycle sub down
|
||||||
|
sub_track_mbtn_right_command=cycle sub
|
||||||
|
sub_track_wheel_down_command=cycle sub
|
||||||
|
sub_track_wheel_up_command=cycle sub down
|
||||||
|
|
||||||
|
# chapter skip buttons mouse actions
|
||||||
|
chapter_prev_mbtn_left_command=add chapter -1
|
||||||
|
chapter_prev_mbtn_mid_command=show-text ${chapter-list} 3000
|
||||||
|
chapter_prev_mbtn_right_command=script-binding select/select-chapter; script-message-to modernz osc-hide
|
||||||
|
|
||||||
|
chapter_next_mbtn_left_command=add chapter 1
|
||||||
|
chapter_next_mbtn_mid_command=show-text ${chapter-list} 3000
|
||||||
|
chapter_next_mbtn_right_command=script-binding select/select-chapter; script-message-to modernz osc-hide
|
||||||
|
|
||||||
|
# chapter title (below seekbar) mouse actions
|
||||||
|
chapter_title_mbtn_left_command=script-binding select/select-chapter; script-message-to modernz osc-hide
|
||||||
|
chapter_title_mbtn_right_command=show-text ${chapter-list} 3000
|
||||||
|
|
||||||
|
# playlist skip buttons mouse actions
|
||||||
|
playlist_prev_mbtn_left_command=playlist-prev
|
||||||
|
playlist_prev_mbtn_mid_command=show-text ${playlist} 3000
|
||||||
|
playlist_prev_mbtn_right_command=script-binding select/select-playlist; script-message-to modernz osc-hide
|
||||||
|
|
||||||
|
playlist_next_mbtn_left_command=playlist-next
|
||||||
|
playlist_next_mbtn_mid_command=show-text ${playlist} 3000
|
||||||
|
playlist_next_mbtn_right_command=script-binding select/select-playlist; script-message-to modernz osc-hide
|
||||||
|
|
||||||
|
# fullscreen button mouse actions
|
||||||
|
fullscreen_mbtn_left_command=cycle fullscreen
|
||||||
|
fullscreen_mbtn_right_command=cycle window-maximized
|
||||||
|
|
||||||
|
# info button mouse actions
|
||||||
|
info_mbtn_left_command=script-binding stats/display-page-1-toggle
|
||||||
3791
submodules/ModernZ/modernz.lua
Normal file
3791
submodules/ModernZ/modernz.lua
Normal file
File diff suppressed because it is too large
Load Diff
4
submodules/mpv-anilist-updater/.gitignore
vendored
Normal file
4
submodules/mpv-anilist-updater/.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
anilistToken.txt
|
||||||
|
.pylintrc
|
||||||
|
pyrightconfig.json
|
||||||
|
.vscode
|
||||||
21
submodules/mpv-anilist-updater/LICENSE
Normal file
21
submodules/mpv-anilist-updater/LICENSE
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2024 AzuredBlue
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
166
submodules/mpv-anilist-updater/README.md
Normal file
166
submodules/mpv-anilist-updater/README.md
Normal file
@@ -0,0 +1,166 @@
|
|||||||
|
# mpv-anilist-updater
|
||||||
|
|
||||||
|
A script for MPV that automatically updates your AniList based on the file you just watched.
|
||||||
|
|
||||||
|
> [!IMPORTANT]
|
||||||
|
> By default, the anime must be set to "watching", "planning" or "rewatching" to update progress. This is done in order to prevent updating the wrong show.<br>
|
||||||
|
> **Recommendation:** Check out the configuration options in your `anilistUpdater.conf` file to customize the script to your needs. See the [Configuration](#configuration-anilistupdaterconf) section for details.<br>
|
||||||
|
|
||||||
|
> [!TIP]
|
||||||
|
> In order for the script to work properly, make sure your files are named correctly:<br>
|
||||||
|
>
|
||||||
|
> - Either the file or folder its in must have the anime title in it<br>
|
||||||
|
> - The file must have the episode number in it (absolute numbering should work)<br>
|
||||||
|
> - In case of remakes, specify the year of the remake to ensure it updates the proper one<br>
|
||||||
|
>
|
||||||
|
> To avoid the script running and making useless API calls, you can set one or more directories in the config file. See the [Configuration](#configuration-anilistupdaterconf) section below.
|
||||||
|
|
||||||
|
For any issues, you can either open an issue on here, or message me on discord (azuredblue)
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
You will need Python 3 installed, as well as the libraries `guessit` and `requests`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pip install guessit requests
|
||||||
|
```
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
Simply download the `anilistUpdater` folder and put it in your mpv scripts folder, or download the contents and make the folder yourself.
|
||||||
|
|
||||||
|
You **WILL** need an AniList access token for it to work:
|
||||||
|
|
||||||
|
1. Visit `https://anilist.co/api/v2/oauth/authorize?client_id=20740&response_type=token`
|
||||||
|
2. Authorize the app
|
||||||
|
3. Copy the token
|
||||||
|
4. Create an `anilistToken.txt` file in the `anilistUpdater` folder (if not already there) and paste the token there.
|
||||||
|
|
||||||
|
This .txt file is also used to cache your AniList user id and to cache recently seen shows, avoiding extra API Calls.
|
||||||
|
This token is what allows the script to update the anime episode count and make api requests, it is not used for anything else.
|
||||||
|
|
||||||
|
## Configuration (`anilistUpdater.conf`)
|
||||||
|
|
||||||
|
> [!IMPORTANT]
|
||||||
|
> The config file is only generated after you run mpv at least once with the script installed. If the file is not created due to lack of write permissions, you can either run mpv as administrator once to generate it, or create the file manually in the appropriate directory. It is recommended to be in the `script-opts` directory.
|
||||||
|
|
||||||
|
When you first run the script, it will automatically create a configuration file called `anilistUpdater.conf` if it does not already exist. This file is typically created in your mpv `script-opts` directory where you have mpv installed. If it cannot be created there, it will try the `scripts` directory or your mpv config directory (e.g. `~/.config/mpv/` on Linux, `%APPDATA%/mpv/` on Windows).
|
||||||
|
|
||||||
|
**Config file search order:**
|
||||||
|
|
||||||
|
The script checks for the config file in the following order:
|
||||||
|
|
||||||
|
1. `script-opts` directory (recommended)
|
||||||
|
2. `scripts` directory (where the script itself is located)
|
||||||
|
3. mpv config directory (e.g. `~/.config/mpv/` or `%APPDATA%/mpv/`)
|
||||||
|
|
||||||
|
If the config file exists in more than one location, the one in the highest-priority directory (script-opts) will be used. For example, if you have a config in both `script-opts` and `scripts`, the one in `script-opts` will take precedence.
|
||||||
|
|
||||||
|
**You should edit this file to change any options.**
|
||||||
|
|
||||||
|
### Example `anilistUpdater.conf`
|
||||||
|
|
||||||
|
```ini
|
||||||
|
# Use 'yes' or 'no' for boolean options below
|
||||||
|
# Example for multiple directories (comma or semicolon separated):
|
||||||
|
# DIRECTORIES=D:/Torrents,D:/Anime
|
||||||
|
# or
|
||||||
|
# DIRECTORIES=D:/Torrents;D:/Anime
|
||||||
|
DIRECTORIES=
|
||||||
|
UPDATE_PERCENTAGE=85
|
||||||
|
SET_COMPLETED_TO_REWATCHING_ON_FIRST_EPISODE=no
|
||||||
|
UPDATE_PROGRESS_WHEN_REWATCHING=yes
|
||||||
|
SET_TO_COMPLETED_AFTER_LAST_EPISODE_CURRENT=yes
|
||||||
|
SET_TO_COMPLETED_AFTER_LAST_EPISODE_REWATCHING=yes
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Option Descriptions
|
||||||
|
|
||||||
|
- **DIRECTORIES**: Comma or semicolon separated list of directories. If empty, the script works for every video. Example: `DIRECTORIES=D:/Torrents,D:/Anime`
|
||||||
|
- Restricting directories only prevents the script from automatically updating AniList for files outside the specified directories. Manual actions using the keybinds (Ctrl+A, Ctrl+B, Ctrl+D) will still work for any file, regardless of its location.
|
||||||
|
- **UPDATE_PERCENTAGE**: Number (0-100). The percentage of the video you need to watch before it updates AniList automatically. Default is `85`.
|
||||||
|
- **SET_COMPLETED_TO_REWATCHING_ON_FIRST_EPISODE**: `yes`/`no`. If `yes`, when watching episode 1 of a completed anime, set it to rewatching and update progress. Default is `no`.
|
||||||
|
- **UPDATE_PROGRESS_WHEN_REWATCHING**: `yes`/`no`. If `yes`, allow updating progress for anime set to rewatching. Default is `yes`.
|
||||||
|
- **SET_TO_COMPLETED_AFTER_LAST_EPISODE_CURRENT**: `yes`/`no`. If `yes`, set to COMPLETED after last episode if status was CURRENT. **Default is `yes`.**
|
||||||
|
- **SET_TO_COMPLETED_AFTER_LAST_EPISODE_REWATCHING**: `yes`/`no`. If `yes`, set to COMPLETED after last episode if status was REPEATING (rewatching). Default is `yes`.
|
||||||
|
|
||||||
|
> [!NOTE]
|
||||||
|
> All boolean options must be `yes` or `no` (not `true`/`false`).
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
This script has 3 keybinds:
|
||||||
|
|
||||||
|
- Ctrl + A: Manually updates your AniList with the current episode you are watching.
|
||||||
|
- Ctrl + B: Opens the AniList page of the anime you are watching on your browser. Useful to see if it guessed the anime correctly.
|
||||||
|
- Ctrl + D: Opens the folder where the current video is playing. Useful if you have "your own" anime library, and navigating through folders is a pain.
|
||||||
|
|
||||||
|
The script will automatically update your AniList when the video you are watching reaches 85% completion (or the percentage you set in the config file).
|
||||||
|
|
||||||
|
You can change the keybinds in your input.conf:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
A script-binding update_anilist
|
||||||
|
B script-binding launch_anilist
|
||||||
|
D script-binding open_folder
|
||||||
|
```
|
||||||
|
|
||||||
|
Or in the `.lua` file:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
mp.add_key_binding('ctrl+a', 'update_anilist', function()
|
||||||
|
update_anilist("update")
|
||||||
|
end)
|
||||||
|
|
||||||
|
mp.add_key_binding('ctrl+b', 'launch_anilist', function()
|
||||||
|
update_anilist("launch")
|
||||||
|
end)
|
||||||
|
|
||||||
|
mp.add_key_binding('ctrl+d', 'open_folder', open_folder)
|
||||||
|
```
|
||||||
|
|
||||||
|
## How It Works
|
||||||
|
|
||||||
|
The script uses Guessit to try to get as much information as possible from the file name.
|
||||||
|
|
||||||
|
If the "episode" and "season" guess are before the title, it will consider that title wrong and try to get the title from the name of the folder it is in.
|
||||||
|
|
||||||
|
If the torrent file has absolute numbering (looking at you, SubsPlease), it will try to guess the season and episode by:
|
||||||
|
|
||||||
|
1. Searching for the anime name on the AniList API.
|
||||||
|
2. Get all results with a similar name, whose format are `TV` and the duration greater than 21 minutes.
|
||||||
|
3. Sort them based on release date.
|
||||||
|
4. Get the season based on the absolute episode number
|
||||||
|
|
||||||
|
It is not a flawless method. It won't work properly if the anime has seasons as ONA's. If it doesn't work properly, consider
|
||||||
|
changing the episode number to the normal format yourself, or simply give up on that series.
|
||||||
|
|
||||||
|
## FAQ (Probably)
|
||||||
|
|
||||||
|
**Q: On what formats does it work?**
|
||||||
|
|
||||||
|
A: It should work on most formats as long as the name is present in the file itself or the folder name.
|
||||||
|
|
||||||
|
`[SubsPlease] Boku no Hero Academia - 152 (1080p) [AAC292E7].mkv` will be detected as S7 E14 and updated accordingly.
|
||||||
|
|
||||||
|
`E12 - Welcome [F1119374].mkv` will work if the folder that it is in has `86` in the name. If it has `86 Part 2` then it should be `Episode 1`
|
||||||
|
|
||||||
|
If it does not, try changing the name of the file / folder, so the search has a better chance at finding it
|
||||||
|
|
||||||
|
**Q: Can I see which anime got detected before it updates?**
|
||||||
|
|
||||||
|
A: Ctrl + B will launch the AniList page of the anime it detects. To see more debug info launch via command line with `mpv file.mkv` or press the `` ` `` keybind to open the console.
|
||||||
|
|
||||||
|
**Q: Can it wrongfully update my anime?**
|
||||||
|
|
||||||
|
A: No, AniList's API does not allow updating an anime which isnt on your watch list. If it didn't detect your anime correctly, then it will
|
||||||
|
simply error.
|
||||||
|
|
||||||
|
**Q: It does not work with X format. What do I do?**
|
||||||
|
|
||||||
|
A: You can try launching the file through the command line with `mpv file.mkv` or opening the console through the keybind \` and see `Guessed name: X`. Try changing the file's name or folder so it has
|
||||||
|
a better chance at guessing the anime. If it still doesn't work, try opening a GitHub issue or messaging me on discord (azuredblue).
|
||||||
|
|
||||||
|
## Credits
|
||||||
|
|
||||||
|
This script was inspired by [mpv-open-anilist-page](https://github.com/ehoneyse/mpv-open-anilist-page) by ehoneyse.
|
||||||
727
submodules/mpv-anilist-updater/anilistUpdater/anilistUpdater.py
Normal file
727
submodules/mpv-anilist-updater/anilistUpdater/anilistUpdater.py
Normal file
@@ -0,0 +1,727 @@
|
|||||||
|
"""
|
||||||
|
mpv-anilist-updater: Automatically updates your AniList based on the file you just watched in MPV.
|
||||||
|
|
||||||
|
This script parses anime filenames, determines the correct AniList entry, and updates your progress
|
||||||
|
or status accordingly.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Configuration options for anilistUpdater (set in anilistUpdater.conf):
|
||||||
|
#
|
||||||
|
# DIRECTORIES: List or comma/semicolon-separated string. The directories the script will work on. Leaving it empty will make it work on every video you watch with mpv. Example: DIRECTORIES = ["D:/Torrents", "D:/Anime"]
|
||||||
|
#
|
||||||
|
# UPDATE_PERCENTAGE: Integer (0-100). The percentage of the video you need to watch before it updates AniList automatically. Default is 85 (usually before the ED of a usual episode duration).
|
||||||
|
#
|
||||||
|
# SET_COMPLETED_TO_REWATCHING_ON_FIRST_EPISODE: Boolean. If true, when watching episode 1 of a completed anime, set it to rewatching and update progress.
|
||||||
|
#
|
||||||
|
# UPDATE_PROGRESS_WHEN_REWATCHING: Boolean. If true, allow updating progress for anime set to rewatching. This is for if you want to set anime to rewatching manually, but still update progress automatically.
|
||||||
|
#
|
||||||
|
# SET_TO_COMPLETED_AFTER_LAST_EPISODE_CURRENT: Boolean. If true, set to COMPLETED after last episode if status was CURRENT.
|
||||||
|
#
|
||||||
|
# SET_TO_COMPLETED_AFTER_LAST_EPISODE_REWATCHING: Boolean. If true, set to COMPLETED after last episode if status was REPEATING (rewatching).
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
import webbrowser
|
||||||
|
import time
|
||||||
|
import ast
|
||||||
|
import hashlib
|
||||||
|
import re
|
||||||
|
import json
|
||||||
|
import requests
|
||||||
|
from guessit import guessit
|
||||||
|
|
||||||
|
class AniListUpdater:
|
||||||
|
"""
|
||||||
|
Handles AniList authentication, file parsing, API requests, and updating anime progress/status.
|
||||||
|
"""
|
||||||
|
ANILIST_API_URL = 'https://graphql.anilist.co'
|
||||||
|
TOKEN_PATH = os.path.join(os.path.dirname(__file__), 'anilistToken.txt')
|
||||||
|
OPTIONS = "--excludes country --excludes language --type episode"
|
||||||
|
CACHE_REFRESH_RATE = 24 * 60 * 60
|
||||||
|
|
||||||
|
# Load token and user id
|
||||||
|
def __init__(self, options):
|
||||||
|
"""
|
||||||
|
Initializes the AniListUpdater, loading the access token and user ID.
|
||||||
|
"""
|
||||||
|
self.access_token = self.load_access_token() # Replace token here if you don't use the .txt
|
||||||
|
self.user_id = self.get_user_id()
|
||||||
|
self.options = options
|
||||||
|
|
||||||
|
# Load token from anilistToken.txt
|
||||||
|
def load_access_token(self):
|
||||||
|
"""
|
||||||
|
Loads the AniList access token from the token file.
|
||||||
|
Returns:
|
||||||
|
str or None: The access token, or None if not found.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
with open(self.TOKEN_PATH, 'r', encoding='utf-8') as file:
|
||||||
|
content = file.read().strip()
|
||||||
|
if ':' in content:
|
||||||
|
token = content.split(':', 1)[1].splitlines()[0]
|
||||||
|
return token
|
||||||
|
|
||||||
|
return content
|
||||||
|
except Exception as e:
|
||||||
|
print(f'Error reading access token: {e}')
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Load user id from file, if not then make api request and save it.
|
||||||
|
def get_user_id(self):
|
||||||
|
"""
|
||||||
|
Loads the AniList user ID from the token file, or fetches and caches it if not present.
|
||||||
|
Returns:
|
||||||
|
int or None: The user ID, or None if not found.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
with open(self.TOKEN_PATH, 'r', encoding='utf-8') as file:
|
||||||
|
content = file.read().strip()
|
||||||
|
if ':' in content:
|
||||||
|
return int(content.split(':')[0])
|
||||||
|
except Exception as e:
|
||||||
|
print(f'Error reading user ID: {e}')
|
||||||
|
|
||||||
|
query = '''
|
||||||
|
query {
|
||||||
|
Viewer {
|
||||||
|
id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
'''
|
||||||
|
response = self.make_api_request(query, None, self.access_token)
|
||||||
|
if response and 'data' in response:
|
||||||
|
user_id = response['data']['Viewer']['id']
|
||||||
|
self.save_user_id(user_id)
|
||||||
|
return user_id
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Cache user id
|
||||||
|
def save_user_id(self, user_id):
|
||||||
|
"""
|
||||||
|
Saves the user ID to the token file, prepending it to the existing content.
|
||||||
|
Args:
|
||||||
|
user_id (int): The AniList user ID.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
with open(self.TOKEN_PATH, 'r+', encoding='utf-8') as file:
|
||||||
|
content = file.read()
|
||||||
|
file.seek(0)
|
||||||
|
file.write(f'{user_id}:{content}')
|
||||||
|
except Exception as e:
|
||||||
|
print(f'Error saving user ID: {e}')
|
||||||
|
|
||||||
|
def cache_to_file(self, path, guessed_name, result):
|
||||||
|
"""
|
||||||
|
Appends a cache entry to the token file for a given file path and guessed anime name.
|
||||||
|
Args:
|
||||||
|
path (str): The file path.
|
||||||
|
guessed_name (str): The guessed anime name.
|
||||||
|
result (tuple): The result to cache.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
with open(self.TOKEN_PATH, 'a', encoding='utf-8') as file:
|
||||||
|
# Epoch Time, hash of the path, guessed name, result
|
||||||
|
file.write(f'\n{time.time()};;{self.hash_path(os.path.dirname(path))};;{guessed_name};;{result}')
|
||||||
|
except Exception as e:
|
||||||
|
print(f'Error trying to cache {result}: {e}')
|
||||||
|
|
||||||
|
def hash_path(self, path):
|
||||||
|
"""
|
||||||
|
Returns a SHA256 hash of the given path.
|
||||||
|
Args:
|
||||||
|
path (str): The path to hash.
|
||||||
|
Returns:
|
||||||
|
str: The hashed path.
|
||||||
|
"""
|
||||||
|
return hashlib.sha256(path.encode('utf-8')).hexdigest()
|
||||||
|
|
||||||
|
def check_and_clean_cache(self, path, guessed_name):
|
||||||
|
"""
|
||||||
|
Checks the cache for a matching entry and cleans out expired entries.
|
||||||
|
Args:
|
||||||
|
path (str): The file path.
|
||||||
|
guessed_name (str): The guessed anime name.
|
||||||
|
Returns:
|
||||||
|
tuple: (cached_result, line_index) or (None, None) if not found.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
valid_lines = []
|
||||||
|
unique = set()
|
||||||
|
path = self.hash_path(os.path.dirname(path))
|
||||||
|
cached_result = (None, None)
|
||||||
|
|
||||||
|
with open(self.TOKEN_PATH, 'r+', encoding='utf-8') as file:
|
||||||
|
orig_lines = file.readlines()
|
||||||
|
|
||||||
|
for line in orig_lines:
|
||||||
|
if line.strip():
|
||||||
|
if ';;' in line:
|
||||||
|
epoch, dir_path, guess, result = line.strip().split(';;')
|
||||||
|
|
||||||
|
if time.time() - float(epoch) < self.CACHE_REFRESH_RATE and (dir_path, guess) not in unique:
|
||||||
|
unique.add((dir_path, guess))
|
||||||
|
valid_lines.append(line)
|
||||||
|
|
||||||
|
if dir_path == path and guess == guessed_name:
|
||||||
|
cached_result = (result, len(valid_lines) - 1)
|
||||||
|
else:
|
||||||
|
valid_lines.append(line)
|
||||||
|
|
||||||
|
if valid_lines != orig_lines:
|
||||||
|
with open(self.TOKEN_PATH, 'w', encoding='utf-8') as file:
|
||||||
|
file.writelines(valid_lines)
|
||||||
|
|
||||||
|
return cached_result
|
||||||
|
except Exception as e:
|
||||||
|
print(f'Error trying to read cache file: {e}')
|
||||||
|
|
||||||
|
def update_cache(self, path, guessed_name, result, index):
|
||||||
|
"""
|
||||||
|
Updates a cache entry at the given index with new data.
|
||||||
|
Args:
|
||||||
|
path (str): The file path.
|
||||||
|
guessed_name (str): The guessed anime name.
|
||||||
|
result (tuple): The result to cache.
|
||||||
|
index (int): The line index in the cache file.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
with open(self.TOKEN_PATH, 'r', encoding='utf-8') as file:
|
||||||
|
lines = file.readlines()
|
||||||
|
|
||||||
|
if 0 <= index < len(lines):
|
||||||
|
# Update the line at the given index with the new cache data
|
||||||
|
updated_line = f'{time.time()};;{self.hash_path(os.path.dirname(path))};;{guessed_name};;{result}\n' if result is not None else ''
|
||||||
|
lines[index] = updated_line
|
||||||
|
|
||||||
|
# Write the updated lines back to the file
|
||||||
|
with open(self.TOKEN_PATH, 'w', encoding='utf-8') as file:
|
||||||
|
file.writelines(lines)
|
||||||
|
|
||||||
|
else:
|
||||||
|
print(f"Invalid index {index} for updating cache.")
|
||||||
|
except Exception as e:
|
||||||
|
print(f'Error trying to update cache file: {e}')
|
||||||
|
|
||||||
|
# Function to make an api request to AniList's api
|
||||||
|
def make_api_request(self, query, variables=None, access_token=None):
|
||||||
|
"""
|
||||||
|
Makes a POST request to the AniList GraphQL API.
|
||||||
|
Args:
|
||||||
|
query (str): The GraphQL query string.
|
||||||
|
variables (dict, optional): Variables for the query.
|
||||||
|
access_token (str, optional): AniList access token.
|
||||||
|
Returns:
|
||||||
|
dict or None: The API response as a dict, or None on error.
|
||||||
|
"""
|
||||||
|
headers = {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Accept': 'application/json'
|
||||||
|
}
|
||||||
|
|
||||||
|
if access_token:
|
||||||
|
headers['Authorization'] = f'Bearer {access_token}'
|
||||||
|
|
||||||
|
response = requests.post(self.ANILIST_API_URL, json={'query': query, 'variables': variables}, headers=headers, timeout=10)
|
||||||
|
# print(f"Made an API Query with: Query: {query}\nVariables: {variables} ")
|
||||||
|
if response.status_code == 200:
|
||||||
|
return response.json()
|
||||||
|
print(f'API request failed: {response.status_code} - {response.text}\nQuery: {query}\nVariables: {variables}')
|
||||||
|
return None
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def season_order(season):
|
||||||
|
"""
|
||||||
|
Returns a numeric order for seasons for sorting.
|
||||||
|
Args:
|
||||||
|
season (str): The season name (WINTER, SPRING, SUMMER, FALL).
|
||||||
|
Returns:
|
||||||
|
int: The order value.
|
||||||
|
"""
|
||||||
|
return {'WINTER': 1, 'SPRING': 2, 'SUMMER': 3, 'FALL': 4}.get(season, 5)
|
||||||
|
|
||||||
|
def filter_valid_seasons(self, seasons):
|
||||||
|
"""
|
||||||
|
Filters and sorts valid TV seasons for absolute numbering logic.
|
||||||
|
Args:
|
||||||
|
seasons (list): List of season dicts from AniList API.
|
||||||
|
Returns:
|
||||||
|
list: Filtered and sorted list of seasons.
|
||||||
|
"""
|
||||||
|
# Filter only to those whose format is TV and duration > 21 OR those who have no duration and are releasing.
|
||||||
|
# This is due to newly added anime having duration as null
|
||||||
|
seasons = [
|
||||||
|
season for season in seasons
|
||||||
|
if ((season['duration'] is None and season['status'] == 'RELEASING') or
|
||||||
|
(season['duration'] is not None and season['duration'] > 21)) and season['format'] == 'TV'
|
||||||
|
]
|
||||||
|
# One of the problems with this filter is needing the format to be 'TV'
|
||||||
|
# But if accepted any format, it would also include many ONA's which arent included in absolute numbering.
|
||||||
|
|
||||||
|
# Sort them based on release date
|
||||||
|
seasons = sorted(seasons, key=lambda x: (x['seasonYear'] if x['seasonYear'] else float("inf"), self.season_order(x['season'] if x['season'] else float("inf"))))
|
||||||
|
return seasons
|
||||||
|
|
||||||
|
# Finds the season and episode of an anime with absolute numbering
|
||||||
|
def find_season_and_episode(self, seasons, absolute_episode):
|
||||||
|
"""
|
||||||
|
Finds the correct season and relative episode for an absolute episode number.
|
||||||
|
Args:
|
||||||
|
seasons (list): List of season dicts.
|
||||||
|
absolute_episode (int): The absolute episode number.
|
||||||
|
Returns:
|
||||||
|
tuple: (season_id, season_title, progress, episodes, relative_episode)
|
||||||
|
"""
|
||||||
|
accumulated_episodes = 0
|
||||||
|
for season in seasons:
|
||||||
|
season_episodes = season.get('episodes', 12) if season.get('episodes') else 12
|
||||||
|
|
||||||
|
if accumulated_episodes + season_episodes >= absolute_episode:
|
||||||
|
return (
|
||||||
|
season.get('id'),
|
||||||
|
season.get('title', {}).get('romaji'),
|
||||||
|
season.get('mediaListEntry', {}).get('progress') if season.get('mediaListEntry') else None,
|
||||||
|
season.get('episodes'),
|
||||||
|
absolute_episode - accumulated_episodes
|
||||||
|
)
|
||||||
|
accumulated_episodes += season_episodes
|
||||||
|
return (None, None, None, None, None)
|
||||||
|
|
||||||
|
def handle_filename(self, filename):
|
||||||
|
"""
|
||||||
|
Main entry point for handling a file: parses, checks cache, updates AniList, and manages cache.
|
||||||
|
Args:
|
||||||
|
filename (str): The path to the video file.
|
||||||
|
"""
|
||||||
|
file_info = self.parse_filename(filename)
|
||||||
|
cached_result, line_index = self.check_and_clean_cache(filename, file_info.get('name'))
|
||||||
|
# str -> tuple
|
||||||
|
if cached_result:
|
||||||
|
try:
|
||||||
|
cached_result = ast.literal_eval(cached_result)
|
||||||
|
if not isinstance(cached_result, (tuple, list)):
|
||||||
|
cached_result = None
|
||||||
|
except Exception:
|
||||||
|
cached_result = None
|
||||||
|
else:
|
||||||
|
cached_result = None
|
||||||
|
|
||||||
|
# True if:
|
||||||
|
# Is not cached
|
||||||
|
# Tries to update and current episode is not the next one.
|
||||||
|
# It is not in your watching/planning list.
|
||||||
|
# This means that for shows with absolute numbering, if it updates, it will always call the API
|
||||||
|
# Since it needs to convert from absolute to relative.
|
||||||
|
if cached_result is None or (cached_result and (file_info.get('episode') != cached_result[2] + 1) and sys.argv[2] != 'launch'):
|
||||||
|
result = self.get_anime_info_and_progress(file_info.get('name'), file_info.get('episode'), file_info.get('year'))
|
||||||
|
result = self.update_episode_count(result) # Returns either the same, or the updated result
|
||||||
|
|
||||||
|
# If it returned a result and the progress isnt None, then put it in cache, since it wasn't.
|
||||||
|
if result and result[2] is not None:
|
||||||
|
if line_index is not None:
|
||||||
|
print(f'Updating cache to: {result}')
|
||||||
|
self.update_cache(filename, file_info.get('name'), result, line_index)
|
||||||
|
else:
|
||||||
|
print(f'Not found in cache! Adding to file... {result}')
|
||||||
|
self.cache_to_file(filename, file_info.get('name'), result)
|
||||||
|
|
||||||
|
# True for opening AniList and updating next episode.
|
||||||
|
else:
|
||||||
|
print(f'Found in cache! {cached_result}')
|
||||||
|
# Only proceed if cached_result is a tuple/list and has enough elements
|
||||||
|
if isinstance(cached_result, (tuple, list)) and len(cached_result) >= 4:
|
||||||
|
# Change to the episode that needs to be updated
|
||||||
|
if len(cached_result) > 5:
|
||||||
|
cached_result = tuple(cached_result[:4]) + (file_info.get('episode'),) + tuple(cached_result[5:])
|
||||||
|
else:
|
||||||
|
cached_result = tuple(cached_result[:4]) + (file_info.get('episode'),)
|
||||||
|
# Ensure tuple is 6 elements (pad with "CURRENT" if needed)
|
||||||
|
if len(cached_result) == 5:
|
||||||
|
cached_result = cached_result + ("CURRENT",)
|
||||||
|
result = self.update_episode_count(cached_result)
|
||||||
|
|
||||||
|
# If it's different, update in cache as well.
|
||||||
|
if cached_result != result and result:
|
||||||
|
print(f'Updating cache to: {result}')
|
||||||
|
self.update_cache(filename, file_info.get('name'), result, line_index)
|
||||||
|
|
||||||
|
# If it either errored or couldn't update, retry without cache.
|
||||||
|
if not result:
|
||||||
|
print('Failed to update through cache, retrying without.')
|
||||||
|
# Deleting from the cache
|
||||||
|
self.update_cache(filename, file_info.get('name'), None, line_index)
|
||||||
|
# Retrying
|
||||||
|
self.handle_filename(filename)
|
||||||
|
else:
|
||||||
|
print('Cached result is invalid, ignoring cache.')
|
||||||
|
# Remove invalid cache entry
|
||||||
|
if line_index is not None:
|
||||||
|
self.update_cache(filename, file_info.get('name'), None, line_index)
|
||||||
|
# Retry without cache
|
||||||
|
self.handle_filename(filename)
|
||||||
|
|
||||||
|
return
|
||||||
|
|
||||||
|
# Hardcoded exceptions to fix detection
|
||||||
|
# Easier than just renaming my files 1 by 1 on Qbit
|
||||||
|
# Every exception I find will be added here
|
||||||
|
def fix_filename(self, path_parts):
|
||||||
|
"""
|
||||||
|
Applies hardcoded exceptions and fixes to the filename and folder structure for better title detection.
|
||||||
|
Args:
|
||||||
|
path_parts (list): List of path components.
|
||||||
|
Returns:
|
||||||
|
list: Modified path components.
|
||||||
|
"""
|
||||||
|
guess = guessit(path_parts[-1], self.OPTIONS) # Simply easier for fixing the filename if we have what it is detecting.
|
||||||
|
|
||||||
|
path_parts[-1] = os.path.splitext(path_parts[-1])[0]
|
||||||
|
pattern = r'[\\\/:!\*\?"<>\|\._-]'
|
||||||
|
|
||||||
|
title_depth = -1
|
||||||
|
|
||||||
|
# Fix from folders if the everything is not in the filename
|
||||||
|
if 'title' not in guess:
|
||||||
|
# Depth=2
|
||||||
|
for depth in range(2, min(4, len(path_parts))):
|
||||||
|
folder_guess = guessit(path_parts[-depth], self.OPTIONS)
|
||||||
|
if 'title' in folder_guess:
|
||||||
|
guess['title'] = folder_guess['title']
|
||||||
|
title_depth = -depth
|
||||||
|
break
|
||||||
|
|
||||||
|
if 'title' not in guess:
|
||||||
|
print(f"Couldn't find title in filename '{path_parts[-1]}'! Guess result: {guess}")
|
||||||
|
return path_parts
|
||||||
|
|
||||||
|
# Only clean up titles for some series
|
||||||
|
cleanup_titles = ['Ranma', 'Chi', 'Bleach', 'Link Click']
|
||||||
|
if any(title in guess['title'] for title in cleanup_titles):
|
||||||
|
path_parts[title_depth] = re.sub(pattern, ' ', path_parts[title_depth])
|
||||||
|
path_parts[title_depth] = " ".join(path_parts[title_depth].split())
|
||||||
|
|
||||||
|
if 'Centimeters per Second' == guess['title'] and 5 == guess.get('episode', 0):
|
||||||
|
path_parts[title_depth] = path_parts[title_depth].replace(' 5 ', ' Five ')
|
||||||
|
# For some reason AniList has this film in 3 parts.
|
||||||
|
path_parts[title_depth] = path_parts[title_depth].replace('per Second', 'per Second 3')
|
||||||
|
|
||||||
|
return path_parts
|
||||||
|
|
||||||
|
# Parse the file name using guessit
|
||||||
|
def parse_filename(self, filepath):
|
||||||
|
"""
|
||||||
|
Parses the filename and folder structure to extract anime title, episode, season, and year.
|
||||||
|
Args:
|
||||||
|
filepath (str): The path to the video file.
|
||||||
|
Returns:
|
||||||
|
dict: Parsed info with keys 'name', 'episode', 'year'.
|
||||||
|
"""
|
||||||
|
path_parts = self.fix_filename(filepath.replace('\\', '/').split('/'))
|
||||||
|
filename = path_parts[-1]
|
||||||
|
name, season, part, year = '', '', '', ''
|
||||||
|
episode = 1
|
||||||
|
# First, try to guess from the filename
|
||||||
|
guess = guessit(filename, self.OPTIONS)
|
||||||
|
print(f'File name guess: {filename} -> {dict(guess)}')
|
||||||
|
|
||||||
|
# Episode guess from the title.
|
||||||
|
# Usually, releases are formated [Release Group] Title - S01EX
|
||||||
|
|
||||||
|
# If the episode index is 0, that would mean that the episode is before the title in the filename
|
||||||
|
# Which is a horrible way of formatting it, so assume its wrong
|
||||||
|
|
||||||
|
# If its 1, then the title is probably 0, so its okay. (Unless season is 0)
|
||||||
|
# Really? What is the format "S1E1 - {title}"? That's almost psycopathic.
|
||||||
|
|
||||||
|
# If its >2, theres probably a Release Group and Title / Season / Part, so its good
|
||||||
|
|
||||||
|
episode = guess.get('episode', None)
|
||||||
|
season = guess.get('season', '')
|
||||||
|
part = str(guess.get('part', ''))
|
||||||
|
year = str(guess.get('year', ''))
|
||||||
|
|
||||||
|
# Quick fixes assuming season before episode
|
||||||
|
# 'episode_title': '02' in 'S2 02'
|
||||||
|
if guess.get('episode_title', '').isdigit() and episode is None:
|
||||||
|
print(f'Detected episode in episode_title. Episode: {int(guess.get("episode_title"))}')
|
||||||
|
episode = int(guess.get('episode_title'))
|
||||||
|
|
||||||
|
# 'episode': [86, 13] (EIGHTY-SIX), [1, 2, 3] (RANMA) lol.
|
||||||
|
if isinstance(episode, list):
|
||||||
|
print(f'Detected multiple episodes: {episode}. Picking last one.')
|
||||||
|
episode = episode[-1]
|
||||||
|
|
||||||
|
# 'season': [2, 3] in "S2 03"
|
||||||
|
if isinstance(season, list):
|
||||||
|
print(f'Detected multiple seasons: {season}. Picking first one as season.')
|
||||||
|
if episode is None:
|
||||||
|
print('Episode still not detected. Picking last position of the season list.')
|
||||||
|
episode = season[-1]
|
||||||
|
|
||||||
|
season = season[0]
|
||||||
|
|
||||||
|
episode = episode or 1
|
||||||
|
season = str(season)
|
||||||
|
|
||||||
|
keys = list(guess.keys())
|
||||||
|
episode_index = keys.index('episode') if 'episode' in guess else 1
|
||||||
|
season_index = keys.index('season') if 'season' in guess else -1
|
||||||
|
title_in_filename = 'title' in guess and (episode_index > 0 and (season_index > 0 or season_index == -1))
|
||||||
|
|
||||||
|
# If the title is not in the filename or episode index is 0, try the folder name
|
||||||
|
# If the episode index > 0 and season index > 0, its safe to assume that the title is in the file name
|
||||||
|
|
||||||
|
if title_in_filename:
|
||||||
|
name = guess['title']
|
||||||
|
else:
|
||||||
|
# If it isnt in the name of the file, try to guess using the name of the folder it is stored in
|
||||||
|
|
||||||
|
# Depth=2 folders
|
||||||
|
for depth in [2, 3]:
|
||||||
|
folder_guess = guessit(path_parts[-depth], self.OPTIONS) if len(path_parts) > depth-1 else ''
|
||||||
|
if folder_guess != '':
|
||||||
|
print(f'{depth-1}{"st" if depth-1==1 else "nd"} Folder guess:\n{path_parts[-depth]} -> {dict(folder_guess)}')
|
||||||
|
|
||||||
|
name = str(folder_guess.get('title', ''))
|
||||||
|
season = season or str(folder_guess.get('season', ''))
|
||||||
|
part = part or str(folder_guess.get('part', ''))
|
||||||
|
year = year or str(folder_guess.get('year', ''))
|
||||||
|
|
||||||
|
# If we got the name, its probable we already got season and part from the way folders are usually structured
|
||||||
|
if name != '':
|
||||||
|
break
|
||||||
|
|
||||||
|
# Add season and part if there are
|
||||||
|
if season and (int(season) > 1 or part):
|
||||||
|
name += f" Season {season}"
|
||||||
|
|
||||||
|
if part:
|
||||||
|
name += f" Part {part}"
|
||||||
|
|
||||||
|
print('Guessed name: ' + name)
|
||||||
|
return {
|
||||||
|
'name': name,
|
||||||
|
'episode': episode,
|
||||||
|
'year': year,
|
||||||
|
}
|
||||||
|
|
||||||
|
def get_anime_info_and_progress(self, name, file_progress, year):
|
||||||
|
"""
|
||||||
|
Queries AniList for anime info and user progress for a given title and year.
|
||||||
|
Args:
|
||||||
|
name (str): Anime title.
|
||||||
|
file_progress (int): Episode number from the file.
|
||||||
|
year (str): Year string (may be empty).
|
||||||
|
Returns:
|
||||||
|
tuple: (anime_id, anime_name, current_progress, total_episodes, file_progress, current_status)
|
||||||
|
"""
|
||||||
|
query = '''
|
||||||
|
query($search: String, $year: FuzzyDateInt, $page: Int) {
|
||||||
|
Page(page: $page) {
|
||||||
|
media (search: $search, type: ANIME, startDate_greater: $year) {
|
||||||
|
id
|
||||||
|
title { romaji }
|
||||||
|
season
|
||||||
|
seasonYear
|
||||||
|
episodes
|
||||||
|
duration
|
||||||
|
format
|
||||||
|
status
|
||||||
|
mediaListEntry {
|
||||||
|
status
|
||||||
|
progress
|
||||||
|
media {
|
||||||
|
episodes
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
'''
|
||||||
|
variables = {'search': name, 'year': year or 1, 'page': 1}
|
||||||
|
|
||||||
|
response = self.make_api_request(query, variables, self.access_token)
|
||||||
|
if response and 'data' in response:
|
||||||
|
seasons = response['data']['Page']['media']
|
||||||
|
# This is the first element, which is the same as Media(search: $search)
|
||||||
|
|
||||||
|
if len(seasons) == 0:
|
||||||
|
raise Exception(f"Couldn\'t find an anime from this title! ({name})")
|
||||||
|
|
||||||
|
entry = seasons[0]['mediaListEntry']
|
||||||
|
anime_data = (
|
||||||
|
seasons[0]['id'],
|
||||||
|
seasons[0]['title']['romaji'],
|
||||||
|
entry['progress'] if entry is not None else None,
|
||||||
|
seasons[0]['episodes'],
|
||||||
|
file_progress,
|
||||||
|
entry['status'] if entry is not None else None
|
||||||
|
)
|
||||||
|
# If the episode in the file name is larger than the total amount of episodes
|
||||||
|
# Then they are using absolute numbering format for episodes (looking at you SubsPlease)
|
||||||
|
# Try to guess season and episode.
|
||||||
|
if seasons[0]['episodes'] is not None and file_progress > seasons[0]['episodes']:
|
||||||
|
seasons = self.filter_valid_seasons(seasons)
|
||||||
|
print('Related shows:', ", ".join(season["title"]["romaji"] for season in seasons))
|
||||||
|
|
||||||
|
anime_data = self.find_season_and_episode(seasons, file_progress)
|
||||||
|
|
||||||
|
found_season = next((season for season in seasons if season['id'] == anime_data[0]), None)
|
||||||
|
found_entry = found_season['mediaListEntry'] if found_season and found_season['mediaListEntry'] else None
|
||||||
|
anime_data = (
|
||||||
|
anime_data[0],
|
||||||
|
anime_data[1],
|
||||||
|
anime_data[2],
|
||||||
|
anime_data[3],
|
||||||
|
anime_data[4],
|
||||||
|
found_entry['status'] if found_entry else None
|
||||||
|
)
|
||||||
|
print(f"Final guessed anime: {found_season}")
|
||||||
|
print(f'Absolute episode {file_progress} corresponds to Anime: {anime_data[1]}, Episode: {anime_data[-2]}')
|
||||||
|
else:
|
||||||
|
print(f"Final guessed anime: {seasons[0]}")
|
||||||
|
return anime_data
|
||||||
|
return (None, None, None, None, None, None)
|
||||||
|
|
||||||
|
# Update the anime based on file progress
|
||||||
|
def update_episode_count(self, result):
|
||||||
|
"""
|
||||||
|
Updates the episode count and/or status for an anime entry on AniList, according to user settings.
|
||||||
|
Args:
|
||||||
|
result (tuple): (anime_id, anime_name, current_progress, total_episodes, file_progress, current_status)
|
||||||
|
Returns:
|
||||||
|
tuple or bool: Updated result tuple, or False on failure.
|
||||||
|
"""
|
||||||
|
if result is None:
|
||||||
|
raise Exception('Parameter in update_episode_count is null.')
|
||||||
|
|
||||||
|
anime_id, anime_name, current_progress, total_episodes, file_progress, current_status = result
|
||||||
|
|
||||||
|
# Only launch anilist
|
||||||
|
if sys.argv[2] == 'launch':
|
||||||
|
print(f'Opening AniList for "{anime_name}": https://anilist.co/anime/{anime_id}')
|
||||||
|
webbrowser.open_new_tab(f'https://anilist.co/anime/{anime_id}')
|
||||||
|
return result
|
||||||
|
|
||||||
|
if current_progress is None:
|
||||||
|
raise Exception('Failed to get current episode count. Is it on your list?')
|
||||||
|
|
||||||
|
# Handle completed -> rewatching on first episode
|
||||||
|
if (current_status == 'COMPLETED' and file_progress == 1 and self.options['SET_COMPLETED_TO_REWATCHING_ON_FIRST_EPISODE']):
|
||||||
|
# Needs to update in 2 steps, since AniList doesn't allow setting progress while changing the status from completed to rewatching. If you try, it will just reset the progress to 0.
|
||||||
|
print('Setting status to REPEATING (rewatching) and updating progress for first episode of completed anime.')
|
||||||
|
# Step 1: Set to REPEATING, progress=0
|
||||||
|
query = '''
|
||||||
|
mutation ($mediaId: Int, $progress: Int, $status: MediaListStatus) {
|
||||||
|
SaveMediaListEntry (mediaId: $mediaId, progress: $progress, status: $status) {
|
||||||
|
status
|
||||||
|
id
|
||||||
|
progress
|
||||||
|
}
|
||||||
|
}
|
||||||
|
'''
|
||||||
|
variables = {'mediaId': anime_id, 'progress': 0, 'status': 'REPEATING'}
|
||||||
|
response = self.make_api_request(query, variables, self.access_token)
|
||||||
|
# Step 2: Set progress to 1
|
||||||
|
variables = {'mediaId': anime_id, 'progress': 1}
|
||||||
|
response = self.make_api_request(query, variables, self.access_token)
|
||||||
|
if response and 'data' in response:
|
||||||
|
updated_progress = response['data']['SaveMediaListEntry']['progress']
|
||||||
|
print(f'Episode count updated successfully! New progress: {updated_progress}')
|
||||||
|
return (anime_id, anime_name, updated_progress, total_episodes, 1, 'REPEATING')
|
||||||
|
print('Failed to update episode count.')
|
||||||
|
return False
|
||||||
|
# Handle updating progress for rewatching
|
||||||
|
if (current_status == 'REPEATING' and self.options['UPDATE_PROGRESS_WHEN_REWATCHING']):
|
||||||
|
print('Updating progress for anime set to REPEATING (rewatching).')
|
||||||
|
status_to_set = 'REPEATING'
|
||||||
|
# Only update if status is CURRENT or PLANNING
|
||||||
|
elif current_status in ['CURRENT', 'PLANNING']:
|
||||||
|
# If its lower than the current progress, dont update.
|
||||||
|
if file_progress <= current_progress:
|
||||||
|
raise Exception(f'Episode was not new. Not updating ({file_progress} <= {current_progress})')
|
||||||
|
status_to_set = 'CURRENT' if file_progress != total_episodes else None
|
||||||
|
else:
|
||||||
|
raise Exception(f'Anime is not in a modifiable state (status: {current_status}). Not updating.')
|
||||||
|
|
||||||
|
set_to_completed = False
|
||||||
|
if file_progress == total_episodes:
|
||||||
|
if current_status == 'CURRENT' and self.options['SET_TO_COMPLETED_AFTER_LAST_EPISODE_CURRENT']:
|
||||||
|
set_to_completed = True
|
||||||
|
elif current_status == 'REPEATING' and self.options['SET_TO_COMPLETED_AFTER_LAST_EPISODE_REWATCHING']:
|
||||||
|
set_to_completed = True
|
||||||
|
|
||||||
|
query = '''
|
||||||
|
mutation ($mediaId: Int, $progress: Int, $status: MediaListStatus) {
|
||||||
|
SaveMediaListEntry (mediaId: $mediaId, progress: $progress, status: $status) {
|
||||||
|
status
|
||||||
|
id
|
||||||
|
progress
|
||||||
|
}
|
||||||
|
}
|
||||||
|
'''
|
||||||
|
|
||||||
|
variables = {'mediaId': anime_id, 'progress': file_progress}
|
||||||
|
if status_to_set:
|
||||||
|
variables['status'] = status_to_set
|
||||||
|
|
||||||
|
response = self.make_api_request(query, variables, self.access_token)
|
||||||
|
if response and 'data' in response:
|
||||||
|
updated_progress = response['data']['SaveMediaListEntry']['progress']
|
||||||
|
print(f'Episode count updated successfully! New progress: {updated_progress}')
|
||||||
|
|
||||||
|
if set_to_completed:
|
||||||
|
print('Setting status to COMPLETED after last episode.')
|
||||||
|
complete_query = '''
|
||||||
|
mutation ($mediaId: Int, $status: MediaListStatus) {
|
||||||
|
SaveMediaListEntry (mediaId: $mediaId, status: $status) {
|
||||||
|
status
|
||||||
|
id
|
||||||
|
progress
|
||||||
|
}
|
||||||
|
}
|
||||||
|
'''
|
||||||
|
complete_variables = {'mediaId': anime_id, 'status': 'COMPLETED'}
|
||||||
|
complete_response = self.make_api_request(complete_query, complete_variables, self.access_token)
|
||||||
|
if complete_response and 'data' in complete_response:
|
||||||
|
print('Status set to COMPLETED successfully.')
|
||||||
|
else:
|
||||||
|
print('Failed to set status to COMPLETED.')
|
||||||
|
|
||||||
|
return (anime_id, anime_name, updated_progress, total_episodes, file_progress, current_status)
|
||||||
|
print('Failed to update episode count.')
|
||||||
|
return False
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""
|
||||||
|
Main entry point for the script. Handles encoding and runs the updater.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# Reconfigure to utf-8
|
||||||
|
if sys.stdout.encoding != 'utf-8':
|
||||||
|
try:
|
||||||
|
sys.stdout.reconfigure(encoding='utf-8')
|
||||||
|
sys.stderr.reconfigure(encoding='utf-8')
|
||||||
|
except Exception as e_reconfigure:
|
||||||
|
print(f"Couldn\'t reconfigure stdout/stderr to UTF-8: {e_reconfigure}", file=sys.stderr)
|
||||||
|
# Parse options from argv[3] if present
|
||||||
|
options = {
|
||||||
|
"SET_COMPLETED_TO_REWATCHING_ON_FIRST_EPISODE": False,
|
||||||
|
"UPDATE_PROGRESS_WHEN_REWATCHING": True,
|
||||||
|
"SET_TO_COMPLETED_AFTER_LAST_EPISODE_CURRENT": False,
|
||||||
|
"SET_TO_COMPLETED_AFTER_LAST_EPISODE_REWATCHING": True
|
||||||
|
}
|
||||||
|
if len(sys.argv) > 3:
|
||||||
|
user_options = json.loads(sys.argv[3])
|
||||||
|
options.update(user_options)
|
||||||
|
|
||||||
|
# Pass options to AniListUpdater
|
||||||
|
updater = AniListUpdater(options)
|
||||||
|
updater.handle_filename(sys.argv[1])
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f'ERROR: {e}')
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
262
submodules/mpv-anilist-updater/anilistUpdater/main.lua
Normal file
262
submodules/mpv-anilist-updater/anilistUpdater/main.lua
Normal file
@@ -0,0 +1,262 @@
|
|||||||
|
--[[
|
||||||
|
Configuration options for anilistUpdater (set in anilistUpdater.conf):
|
||||||
|
|
||||||
|
DIRECTORIES: Table or comma/semicolon-separated string. The directories the script will work on. Leaving it empty will make it work on every video you watch with mpv. Example: DIRECTORIES = {"D:/Torrents", "D:/Anime"}
|
||||||
|
|
||||||
|
UPDATE_PERCENTAGE: Number (0-100). The percentage of the video you need to watch before it updates AniList automatically. Default is 85 (usually before the ED of a usual episode duration).
|
||||||
|
|
||||||
|
SET_COMPLETED_TO_REWATCHING_ON_FIRST_EPISODE: Boolean. If true, when watching episode 1 of a completed anime, set it to rewatching and update progress.
|
||||||
|
|
||||||
|
UPDATE_PROGRESS_WHEN_REWATCHING: Boolean. If true, allow updating progress for anime set to rewatching. This is for if you want to set anime to rewatching manually, but still update progress automatically.
|
||||||
|
|
||||||
|
SET_TO_COMPLETED_AFTER_LAST_EPISODE_CURRENT: Boolean. If true, set to COMPLETED after last episode if status was CURRENT.
|
||||||
|
|
||||||
|
SET_TO_COMPLETED_AFTER_LAST_EPISODE_REWATCHING: Boolean. If true, set to COMPLETED after last episode if status was REPEATING (rewatching).
|
||||||
|
]]
|
||||||
|
local utils = require 'mp.utils'
|
||||||
|
local mpoptions = require("mp.options")
|
||||||
|
|
||||||
|
local conf_name = "anilistUpdater.conf"
|
||||||
|
local script_dir = (debug.getinfo(1).source:match("@?(.*/)") or "./")
|
||||||
|
|
||||||
|
-- Try script-opts directory (sibling to scripts)
|
||||||
|
local script_opts_dir = script_dir:match("^(.-)[/\\]scripts[/\\]")
|
||||||
|
|
||||||
|
if script_opts_dir then
|
||||||
|
script_opts_dir = utils.join_path(script_opts_dir, "script-opts")
|
||||||
|
else
|
||||||
|
-- Fallback: try to find mpv config dir
|
||||||
|
script_opts_dir = os.getenv("APPDATA") and utils.join_path(utils.join_path(os.getenv("APPDATA"), "mpv"), "script-opts") or
|
||||||
|
os.getenv("HOME") and utils.join_path(utils.join_path(utils.join_path(os.getenv("HOME"), ".config"), "mpv"), "script-opts") or
|
||||||
|
nil
|
||||||
|
end
|
||||||
|
|
||||||
|
local script_opts_path = script_opts_dir and utils.join_path(script_opts_dir, conf_name) or nil
|
||||||
|
|
||||||
|
-- Try script directory
|
||||||
|
local script_path = utils.join_path(script_dir, conf_name)
|
||||||
|
|
||||||
|
-- Try mpv config directory
|
||||||
|
local mpv_conf_dir = os.getenv("APPDATA") and utils.join_path(os.getenv("APPDATA"), "mpv") or os.getenv("HOME") and
|
||||||
|
utils.join_path(utils.join_path(os.getenv("HOME"), ".config"), "mpv") or nil
|
||||||
|
local mpv_conf_path = mpv_conf_dir and utils.join_path(mpv_conf_dir, conf_name) or nil
|
||||||
|
|
||||||
|
local conf_paths = {script_opts_path, script_path, mpv_conf_path}
|
||||||
|
|
||||||
|
local default_conf = [[
|
||||||
|
# Use 'yes' or 'no' for boolean options below
|
||||||
|
# Example for multiple directories (comma or semicolon separated):
|
||||||
|
# DIRECTORIES=D:/Torrents,D:/Anime
|
||||||
|
# or
|
||||||
|
# DIRECTORIES=D:/Torrents;D:/Anime
|
||||||
|
DIRECTORIES=
|
||||||
|
UPDATE_PERCENTAGE=85
|
||||||
|
SET_COMPLETED_TO_REWATCHING_ON_FIRST_EPISODE=no
|
||||||
|
UPDATE_PROGRESS_WHEN_REWATCHING=yes
|
||||||
|
SET_TO_COMPLETED_AFTER_LAST_EPISODE_CURRENT=yes
|
||||||
|
SET_TO_COMPLETED_AFTER_LAST_EPISODE_REWATCHING=yes
|
||||||
|
]]
|
||||||
|
|
||||||
|
-- Try to find config file
|
||||||
|
local conf_path = nil
|
||||||
|
for _, path in ipairs(conf_paths) do
|
||||||
|
if path then
|
||||||
|
local f = io.open(path, "r")
|
||||||
|
if f then
|
||||||
|
f:close()
|
||||||
|
conf_path = path
|
||||||
|
print("Found config at: " .. path)
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- If not found, try to create in order
|
||||||
|
if not conf_path then
|
||||||
|
for _, path in ipairs(conf_paths) do
|
||||||
|
if path then
|
||||||
|
local f = io.open(path, "w")
|
||||||
|
if f then
|
||||||
|
f:write(default_conf)
|
||||||
|
f:close()
|
||||||
|
conf_path = path
|
||||||
|
print("Created config at: " .. path)
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- If still not found or created, warn and use defaults
|
||||||
|
if not conf_path then
|
||||||
|
mp.msg.warn("Could not find or create anilistUpdater.conf in any known location! Using default options.")
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Now load options as usual
|
||||||
|
local options = {
|
||||||
|
DIRECTORIES = "",
|
||||||
|
UPDATE_PERCENTAGE = 85,
|
||||||
|
SET_COMPLETED_TO_REWATCHING_ON_FIRST_EPISODE = false,
|
||||||
|
UPDATE_PROGRESS_WHEN_REWATCHING = true,
|
||||||
|
SET_TO_COMPLETED_AFTER_LAST_EPISODE_CURRENT = true,
|
||||||
|
SET_TO_COMPLETED_AFTER_LAST_EPISODE_REWATCHING = true
|
||||||
|
}
|
||||||
|
if conf_path then
|
||||||
|
mpoptions.read_options(options, "anilistUpdater")
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Parse DIRECTORIES if it's a string (comma or semicolon separated)
|
||||||
|
if type(options.DIRECTORIES) == "string" and options.DIRECTORIES ~= "" then
|
||||||
|
local dirs = {}
|
||||||
|
for dir in string.gmatch(options.DIRECTORIES, "([^,;]+)") do
|
||||||
|
table.insert(dirs, (dir:gsub("^%s*(.-)%s*$", "%1"))) -- trim
|
||||||
|
end
|
||||||
|
options.DIRECTORIES = dirs
|
||||||
|
elseif type(options.DIRECTORIES) == "string" then
|
||||||
|
options.DIRECTORIES = {}
|
||||||
|
end
|
||||||
|
|
||||||
|
-- When calling Python, pass only the options relevant to it
|
||||||
|
local python_options = {
|
||||||
|
SET_COMPLETED_TO_REWATCHING_ON_FIRST_EPISODE = options.SET_COMPLETED_TO_REWATCHING_ON_FIRST_EPISODE,
|
||||||
|
UPDATE_PROGRESS_WHEN_REWATCHING = options.UPDATE_PROGRESS_WHEN_REWATCHING,
|
||||||
|
SET_TO_COMPLETED_AFTER_LAST_EPISODE_CURRENT = options.SET_TO_COMPLETED_AFTER_LAST_EPISODE_CURRENT,
|
||||||
|
SET_TO_COMPLETED_AFTER_LAST_EPISODE_REWATCHING = options.SET_TO_COMPLETED_AFTER_LAST_EPISODE_REWATCHING
|
||||||
|
}
|
||||||
|
local python_options_json = utils.format_json(python_options)
|
||||||
|
|
||||||
|
DIRECTORIES = options.DIRECTORIES
|
||||||
|
UPDATE_PERCENTAGE = tonumber(options.UPDATE_PERCENTAGE) or 85
|
||||||
|
|
||||||
|
local function path_starts_with_any(path, directories)
|
||||||
|
for _, dir in ipairs(directories) do
|
||||||
|
if path:sub(1, #dir) == dir then
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
function callback(success, result, error)
|
||||||
|
if result.status == 0 then
|
||||||
|
mp.osd_message("Updated anime correctly.", 2)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function get_python_command()
|
||||||
|
local os_name = package.config:sub(1, 1)
|
||||||
|
if os_name == '\\' then
|
||||||
|
-- Windows
|
||||||
|
return "python"
|
||||||
|
else
|
||||||
|
-- Linux
|
||||||
|
return "python3"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local python_command = get_python_command()
|
||||||
|
|
||||||
|
-- Make sure it doesnt trigger twice in 1 video
|
||||||
|
local triggered = false
|
||||||
|
|
||||||
|
-- Function to check if we've reached the user-defined percentage of the video
|
||||||
|
function check_progress()
|
||||||
|
if triggered then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
local percent_pos = mp.get_property_number("percent-pos")
|
||||||
|
|
||||||
|
if percent_pos then
|
||||||
|
if percent_pos >= UPDATE_PERCENTAGE then
|
||||||
|
update_anilist("update")
|
||||||
|
triggered = true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Function to launch the .py script
|
||||||
|
function update_anilist(action)
|
||||||
|
if action == "launch" then
|
||||||
|
mp.osd_message("Launching AniList", 2)
|
||||||
|
end
|
||||||
|
local script_dir = debug.getinfo(1).source:match("@?(.*/)")
|
||||||
|
local directory = mp.get_property("working-directory")
|
||||||
|
-- It seems like in Linux working-directory sometimes returns it without a "/" at the end
|
||||||
|
directory = (directory:sub(-1) == '/' or directory:sub(-1) == '\\') and directory or directory .. '/'
|
||||||
|
-- For some reason, "path" sometimes returns the absolute path, sometimes it doesn't.
|
||||||
|
local file_path = mp.get_property("path")
|
||||||
|
local path = utils.join_path(directory, file_path)
|
||||||
|
|
||||||
|
local table = {}
|
||||||
|
table.name = "subprocess"
|
||||||
|
table.args = {python_command, script_dir .. "anilistUpdater.py", path, action, python_options_json}
|
||||||
|
local cmd = mp.command_native_async(table, callback)
|
||||||
|
end
|
||||||
|
|
||||||
|
mp.observe_property("percent-pos", "number", check_progress)
|
||||||
|
|
||||||
|
-- Reset triggered
|
||||||
|
mp.register_event("file-loaded", function()
|
||||||
|
triggered = false
|
||||||
|
if #DIRECTORIES > 0 then
|
||||||
|
local directory = mp.get_property("working-directory")
|
||||||
|
directory = (directory:sub(-1) == '/' or directory:sub(-1) == '\\') and directory or directory .. '/'
|
||||||
|
local file_path = mp.get_property("path")
|
||||||
|
local path = utils.join_path(directory, file_path)
|
||||||
|
path = path:gsub("\\", "/")
|
||||||
|
|
||||||
|
if not path_starts_with_any(path, DIRECTORIES) then
|
||||||
|
mp.unobserve_property(check_progress)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
|
||||||
|
-- Keybinds, modify as you please
|
||||||
|
mp.add_key_binding('ctrl+a', 'update_anilist', function()
|
||||||
|
update_anilist("update")
|
||||||
|
end)
|
||||||
|
|
||||||
|
mp.add_key_binding('ctrl+b', 'launch_anilist', function()
|
||||||
|
update_anilist("launch")
|
||||||
|
end)
|
||||||
|
|
||||||
|
-- Open the folder that the video is
|
||||||
|
function open_folder()
|
||||||
|
local path = mp.get_property("path")
|
||||||
|
local directory
|
||||||
|
|
||||||
|
if not path then
|
||||||
|
mp.msg.warn("No file is currently playing.")
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
if path:find('\\') then
|
||||||
|
directory = path:match("(.*)\\")
|
||||||
|
elseif path:find('\\\\') then
|
||||||
|
directory = path:match("(.*)\\\\")
|
||||||
|
else
|
||||||
|
directory = mp.get_property("working-directory")
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Use the system command to open the folder in File Explorer
|
||||||
|
local args
|
||||||
|
if package.config:sub(1, 1) == '\\' then
|
||||||
|
-- Windows
|
||||||
|
args = {'explorer', directory}
|
||||||
|
elseif os.getenv("XDG_CURRENT_DESKTOP") or os.getenv("WAYLAND_DISPLAY") or os.getenv("DISPLAY") then
|
||||||
|
-- Linux (assume a desktop environment like GNOME, KDE, etc.)
|
||||||
|
args = {'xdg-open', directory}
|
||||||
|
elseif package.config:sub(1, 1) == '/' then
|
||||||
|
-- macOS
|
||||||
|
args = {'open', directory}
|
||||||
|
end
|
||||||
|
|
||||||
|
mp.command_native({
|
||||||
|
name = "subprocess",
|
||||||
|
args = args,
|
||||||
|
detach = true
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
mp.add_key_binding('ctrl+d', 'open_folder', open_folder)
|
||||||
BIN
submodules/mpv-youtube-queue/.assets/mpv-youtube-queue.png
Normal file
BIN
submodules/mpv-youtube-queue/.assets/mpv-youtube-queue.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 629 KiB |
10
submodules/mpv-youtube-queue/.github/workflows/luackeck.yml
vendored
Normal file
10
submodules/mpv-youtube-queue/.github/workflows/luackeck.yml
vendored
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
name: Luacheck
|
||||||
|
on: push
|
||||||
|
jobs:
|
||||||
|
luacheck:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
- name: Luacheck linter
|
||||||
|
uses: lunarmodules/luacheck@v1
|
||||||
3
submodules/mpv-youtube-queue/.gitignore
vendored
Normal file
3
submodules/mpv-youtube-queue/.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
test.sh
|
||||||
|
.git/*
|
||||||
|
.luarc.json
|
||||||
185
submodules/mpv-youtube-queue/LICENSE
Normal file
185
submodules/mpv-youtube-queue/LICENSE
Normal file
@@ -0,0 +1,185 @@
|
|||||||
|
GNU GENERAL PUBLIC LICENSE
|
||||||
|
Version 3, 29 June 2007
|
||||||
|
|
||||||
|
Copyright © 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
||||||
|
|
||||||
|
Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed.
|
||||||
|
|
||||||
|
Preamble
|
||||||
|
The GNU General Public License is a free, copyleft license for software and other kinds of works.
|
||||||
|
|
||||||
|
The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free software for all its users. We, the Free Software Foundation, use the GNU General Public License for most of our software; it applies also to any other work released this way by its authors. You can apply it to your programs, too.
|
||||||
|
|
||||||
|
When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for them if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and that you know you can do these things.
|
||||||
|
|
||||||
|
To protect your rights, we need to prevent others from denying you these rights or asking you to surrender the rights. Therefore, you have certain responsibilities if you distribute copies of the software, or if you modify it: responsibilities to respect the freedom of others.
|
||||||
|
|
||||||
|
For example, if you distribute copies of such a program, whether gratis or for a fee, you must pass on to the recipients the same freedoms that you received. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights.
|
||||||
|
|
||||||
|
Developers that use the GNU GPL protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License giving you legal permission to copy, distribute and/or modify it.
|
||||||
|
|
||||||
|
For the developers' and authors' protection, the GPL clearly explains that there is no warranty for this free software. For both users' and authors' sake, the GPL requires that modified versions be marked as changed, so that their problems will not be attributed erroneously to authors of previous versions.
|
||||||
|
|
||||||
|
Some devices are designed to deny users access to install or run modified versions of the software inside them, although the manufacturer can do so. This is fundamentally incompatible with the aim of protecting users' freedom to change the software. The systematic pattern of such abuse occurs in the area of products for individuals to use, which is precisely where it is most unacceptable. Therefore, we have designed this version of the GPL to prohibit the practice for those products. If such problems arise substantially in other domains, we stand ready to extend this provision to those domains in future versions of the GPL, as needed to protect the freedom of users.
|
||||||
|
|
||||||
|
Finally, every program is threatened constantly by software patents. States should not allow patents to restrict development and use of software on general-purpose computers, but in those that do, we wish to avoid the special danger that patents applied to a free program could make it effectively proprietary. To prevent this, the GPL assures that patents cannot be used to render the program non-free.
|
||||||
|
|
||||||
|
The precise terms and conditions for copying, distribution and modification follow.
|
||||||
|
|
||||||
|
TERMS AND CONDITIONS
|
||||||
|
0. Definitions.
|
||||||
|
“This License” refers to version 3 of the GNU General Public License.
|
||||||
|
|
||||||
|
“Copyright” also means copyright-like laws that apply to other kinds of works, such as semiconductor masks.
|
||||||
|
|
||||||
|
“The Program” refers to any copyrightable work licensed under this License. Each licensee is addressed as “you”. “Licensees” and “recipients” may be individuals or organizations.
|
||||||
|
|
||||||
|
To “modify” a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission, other than the making of an exact copy. The resulting work is called a “modified version” of the earlier work or a work “based on” the earlier work.
|
||||||
|
|
||||||
|
A “covered work” means either the unmodified Program or a work based on the Program.
|
||||||
|
|
||||||
|
To “propagate” a work means to do anything with it that, without permission, would make you directly or secondarily liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy. Propagation includes copying, distribution (with or without modification), making available to the public, and in some countries other activities as well.
|
||||||
|
|
||||||
|
To “convey” a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction with a user through a computer network, with no transfer of a copy, is not conveying.
|
||||||
|
|
||||||
|
An interactive user interface displays “Appropriate Legal Notices” to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright notice, and (2) tells the user that there is no warranty for the work (except to the extent that warranties are provided), that licensees may convey the work under this License, and how to view a copy of this License. If the interface presents a list of user commands or options, such as a menu, a prominent item in the list meets this criterion.
|
||||||
|
|
||||||
|
1. Source Code.
|
||||||
|
The “source code” for a work means the preferred form of the work for making modifications to it. “Object code” means any non-source form of a work.
|
||||||
|
|
||||||
|
A “Standard Interface” means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language.
|
||||||
|
|
||||||
|
The “System Libraries” of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A “Major Component”, in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used to run it.
|
||||||
|
|
||||||
|
The “Corresponding Source” for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those subprograms and other parts of the work.
|
||||||
|
|
||||||
|
The Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source.
|
||||||
|
|
||||||
|
The Corresponding Source for a work in source code form is that same work.
|
||||||
|
|
||||||
|
2. Basic Permissions.
|
||||||
|
All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program. The output from running a covered work is covered by this License only if the output, given its content, constitutes a covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law.
|
||||||
|
|
||||||
|
You may make, run and propagate covered works that you do not convey, without conditions so long as your license otherwise remains in force. You may convey covered works to others for the sole purpose of having them make modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with the terms of this License in conveying all material for which you do not control copyright. Those thus making or running the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that prohibit them from making any copies of your copyrighted material outside their relationship with you.
|
||||||
|
|
||||||
|
Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary.
|
||||||
|
|
||||||
|
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
|
||||||
|
No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention of such measures.
|
||||||
|
|
||||||
|
When you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users, your or third parties' legal rights to forbid circumvention of technological measures.
|
||||||
|
|
||||||
|
4. Conveying Verbatim Copies.
|
||||||
|
You may convey verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices of the absence of any warranty; and give all recipients a copy of this License along with the Program.
|
||||||
|
|
||||||
|
You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee.
|
||||||
|
|
||||||
|
5. Conveying Modified Source Versions.
|
||||||
|
You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source code under the terms of section 4, provided that you also meet all of these conditions:
|
||||||
|
|
||||||
|
a) The work must carry prominent notices stating that you modified it, and giving a relevant date.
|
||||||
|
b) The work must carry prominent notices stating that it is released under this License and any conditions added under section 7. This requirement modifies the requirement in section 4 to “keep intact all notices”.
|
||||||
|
c) You must license the entire work, as a whole, under this License to anyone who comes into possession of a copy. This License will therefore apply, along with any applicable section 7 additional terms, to the whole of the work, and all its parts, regardless of how they are packaged. This License gives no permission to license the work in any other way, but it does not invalidate such permission if you have separately received it.
|
||||||
|
d) If the work has interactive user interfaces, each must display Appropriate Legal Notices; however, if the Program has interactive interfaces that do not display Appropriate Legal Notices, your work need not make them do so.
|
||||||
|
A compilation of a covered work with other separate and independent works, which are not by their nature extensions of the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or distribution medium, is called an “aggregate” if the compilation and its resulting copyright are not used to limit the access or legal rights of the compilation's users beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause this License to apply to the other parts of the aggregate.
|
||||||
|
|
||||||
|
6. Conveying Non-Source Forms.
|
||||||
|
You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the machine-readable Corresponding Source under the terms of this License, in one of these ways:
|
||||||
|
|
||||||
|
a) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by the Corresponding Source fixed on a durable physical medium customarily used for software interchange.
|
||||||
|
b) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by a written offer, valid for at least three years and valid for as long as you offer spare parts or customer support for that product model, to give anyone who possesses the object code either (1) a copy of the Corresponding Source for all the software in the product that is covered by this License, on a durable physical medium customarily used for software interchange, for a price no more than your reasonable cost of physically performing this conveying of source, or (2) access to copy the Corresponding Source from a network server at no charge.
|
||||||
|
c) Convey individual copies of the object code with a copy of the written offer to provide the Corresponding Source. This alternative is allowed only occasionally and noncommercially, and only if you received the object code with such an offer, in accord with subsection 6b.
|
||||||
|
d) Convey the object code by offering access from a designated place (gratis or for a charge), and offer equivalent access to the Corresponding Source in the same way through the same place at no further charge. You need not require recipients to copy the Corresponding Source along with the object code. If the place to copy the object code is a network server, the Corresponding Source may be on a different server (operated by you or a third party) that supports equivalent copying facilities, provided you maintain clear directions next to the object code saying where to find the Corresponding Source. Regardless of what server hosts the Corresponding Source, you remain obligated to ensure that it is available for as long as needed to satisfy these requirements.
|
||||||
|
e) Convey the object code using peer-to-peer transmission, provided you inform other peers where the object code and Corresponding Source of the work are being offered to the general public at no charge under subsection 6d.
|
||||||
|
A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, need not be included in conveying the object code work.
|
||||||
|
|
||||||
|
A “User Product” is either (1) a “consumer product”, which means any tangible personal property which is normally used for personal, family, or household purposes, or (2) anything designed or sold for incorporation into a dwelling. In determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. For a particular product received by a particular user, “normally used” refers to a typical or common use of that class of product, regardless of the status of the particular user or of the way in which the particular user actually uses, or expects or is expected to use, the product. A product is a consumer product regardless of whether the product has substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of the product.
|
||||||
|
|
||||||
|
“Installation Information” for a User Product means any methods, procedures, authorization keys, or other information required to install and execute modified versions of a covered work in that User Product from a modified version of its Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code is in no case prevented or interfered with solely because modification has been made.
|
||||||
|
|
||||||
|
If you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not apply if neither you nor any third party retains the ability to install modified object code on the User Product (for example, the work has been installed in ROM).
|
||||||
|
|
||||||
|
The requirement to provide Installation Information does not include a requirement to continue to provide support service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product in which it has been modified or installed. Access to a network may be denied when the modification itself materially and adversely affects the operation of the network or violates the rules and protocols for communication across the network.
|
||||||
|
|
||||||
|
Corresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format that is publicly documented (and with an implementation available to the public in source code form), and must require no special password or key for unpacking, reading or copying.
|
||||||
|
|
||||||
|
7. Additional Terms.
|
||||||
|
“Additional permissions” are terms that supplement the terms of this License by making exceptions from one or more of its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were included in this License, to the extent that they are valid under applicable law. If additional permissions apply only to part of the Program, that part may be used separately under those permissions, but the entire Program remains governed by this License without regard to the additional permissions.
|
||||||
|
|
||||||
|
When you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or from any part of it. (Additional permissions may be written to require their own removal in certain cases when you modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have or can give appropriate copyright permission.
|
||||||
|
|
||||||
|
Notwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by the copyright holders of that material) supplement the terms of this License with terms:
|
||||||
|
|
||||||
|
a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or
|
||||||
|
b) Requiring preservation of specified reasonable legal notices or author attributions in that material or in the Appropriate Legal Notices displayed by works containing it; or
|
||||||
|
c) Prohibiting misrepresentation of the origin of that material, or requiring that modified versions of such material be marked in reasonable ways as different from the original version; or
|
||||||
|
d) Limiting the use for publicity purposes of names of licensors or authors of the material; or
|
||||||
|
e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or
|
||||||
|
f) Requiring indemnification of licensors and authors of that material by anyone who conveys the material (or modified versions of it) with contractual assumptions of liability to the recipient, for any liability that these contractual assumptions directly impose on those licensors and authors.
|
||||||
|
All other non-permissive additional terms are considered “further restrictions” within the meaning of section 10. If the Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with a term that is a further restriction, you may remove that term. If a license document contains a further restriction but permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of that license document, provided that the further restriction does not survive such relicensing or conveying.
|
||||||
|
|
||||||
|
If you add terms to a covered work in accord with this section, you must place, in the relevant source files, a statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms.
|
||||||
|
|
||||||
|
Additional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as exceptions; the above requirements apply either way.
|
||||||
|
|
||||||
|
8. Termination.
|
||||||
|
You may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to propagate or modify it is void, and will automatically terminate your rights under this License (including any patent licenses granted under the third paragraph of section 11).
|
||||||
|
|
||||||
|
However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation.
|
||||||
|
|
||||||
|
Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice.
|
||||||
|
|
||||||
|
Termination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not qualify to receive new licenses for the same material under section 10.
|
||||||
|
|
||||||
|
9. Acceptance Not Required for Having Copies.
|
||||||
|
You are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a covered work, you indicate your acceptance of this License to do so.
|
||||||
|
|
||||||
|
10. Automatic Licensing of Downstream Recipients.
|
||||||
|
Each time you convey a covered work, the recipient automatically receives a license from the original licensors, to run, modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third parties with this License.
|
||||||
|
|
||||||
|
An “entity transaction” is a transaction transferring control of an organization, or substantially all assets of one, or subdividing an organization, or merging organizations. If propagation of a covered work results from an entity transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with reasonable efforts.
|
||||||
|
|
||||||
|
You may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License, and you may not initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using, selling, offering for sale, or importing the Program or any portion of it.
|
||||||
|
|
||||||
|
11. Patents.
|
||||||
|
A “contributor” is a copyright holder who authorizes use under this License of the Program or a work on which the Program is based. The work thus licensed is called the contributor's “contributor version”.
|
||||||
|
|
||||||
|
A contributor's “essential patent claims” are all patent claims owned or controlled by the contributor, whether already acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or selling its contributor version, but do not include claims that would be infringed only as a consequence of further modification of the contributor version. For purposes of this definition, “control” includes the right to grant patent sublicenses in a manner consistent with the requirements of this License.
|
||||||
|
|
||||||
|
Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its contributor version.
|
||||||
|
|
||||||
|
In the following three paragraphs, a “patent license” is any express agreement or commitment, however denominated, not to enforce a patent (such as an express permission to practice a patent or covenant not to sue for patent infringement). To “grant” such a patent license to a party means to make such an agreement or commitment not to enforce a patent against the party.
|
||||||
|
|
||||||
|
If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not available for anyone to copy, free of charge and under the terms of this License, through a publicly available network server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available, or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a manner consistent with the requirements of this License, to extend the patent license to downstream recipients. “Knowingly relying” means you have actual knowledge that, but for the patent license, your conveying the covered work in a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in that country that you have reason to believe are valid.
|
||||||
|
|
||||||
|
If, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is automatically extended to all recipients of the covered work and works based on it.
|
||||||
|
|
||||||
|
A patent license is “discriminatory” if it does not include within the scope of its coverage, prohibits the exercise of, or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You may not convey a covered work if you are a party to an arrangement with a third party that is in the business of distributing software, under which you make payment to the third party based on the extent of your activity of conveying the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a discriminatory patent license (a) in connection with copies of the covered work conveyed by you (or copies made from those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007.
|
||||||
|
|
||||||
|
Nothing in this License shall be construed as excluding or limiting any implied license or other defenses to infringement that may otherwise be available to you under applicable patent law.
|
||||||
|
|
||||||
|
12. No Surrender of Others' Freedom.
|
||||||
|
If conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot convey a covered work so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program.
|
||||||
|
|
||||||
|
13. Use with the GNU Affero General Public License.
|
||||||
|
Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed under version 3 of the GNU Affero General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, but the special requirements of the GNU Affero General Public License, section 13, concerning interaction through a network will apply to the combination as such.
|
||||||
|
|
||||||
|
14. Revised Versions of this License.
|
||||||
|
The Free Software Foundation may publish revised and/or new versions of the GNU General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns.
|
||||||
|
|
||||||
|
Each version is given a distinguishing version number. If the Program specifies that a certain numbered version of the GNU General Public License “or any later version” applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the GNU General Public License, you may choose any version ever published by the Free Software Foundation.
|
||||||
|
|
||||||
|
If the Program specifies that a proxy can decide which future versions of the GNU General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program.
|
||||||
|
|
||||||
|
Later license versions may give you additional or different permissions. However, no additional obligations are imposed on any author or copyright holder as a result of your choosing to follow a later version.
|
||||||
|
|
||||||
|
15. Disclaimer of Warranty.
|
||||||
|
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM “AS IS” WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
||||||
|
|
||||||
|
16. Limitation of Liability.
|
||||||
|
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
|
||||||
|
|
||||||
|
17. Interpretation of Sections 15 and 16.
|
||||||
|
If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee.
|
||||||
|
|
||||||
|
END OF TERMS AND CONDITIONS
|
||||||
93
submodules/mpv-youtube-queue/README.md
Normal file
93
submodules/mpv-youtube-queue/README.md
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
# mpv-youtube-queue
|
||||||
|
|
||||||
|
<div align="center">
|
||||||
|
|
||||||
|
A Lua script that replicates and extends the YouTube "Add to Queue" feature for mpv
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- **Interactive Queue Management:** A menu-driven interface for adding, removing, and rearranging videos in your queue
|
||||||
|
- **yt-dlp Integration:** Gathers video info and allows downloading with any link supported by [yt-dlp](https://github.com/yt-dlp/yt-dlp/blob/master/supportedsites.md "yd-dlp supported sites page")
|
||||||
|
- **Internal Playlist Integration:** Seamlessly integrates with mpv's internal playlist for a unified playback experience
|
||||||
|
- **Customizable Keybindings:** Assign your preferred hotkeys to interact with the currently playing video and queue
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
This script requires the following software to be installed on the system
|
||||||
|
|
||||||
|
- One of [xclip](https://github.com/astrand/xclip), [wl-clipboard](https://github.com/bugaevc/wl-clipboard), or any command-line utility that can paste from the system clipboard
|
||||||
|
- Windows users can utilize `Get-Clipboard` from powershell by setting the `clipboard_command` in `mpv-youtube-queue.conf` file to the following: `clipboard_command=powershell -command Get-Clipboard`
|
||||||
|
- [yt-dlp](https://github.com/yt-dlp/yt-dlp)
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
- Copy `mpv-youtube-queue.lua` script to your `~~/scripts` directory
|
||||||
|
- `~/.config/mpv/scripts` on Linux
|
||||||
|
- `%APPDATA%\mpv\scripts` on Windows
|
||||||
|
- Optionally copy `mpv-youtube-queue.conf` to the `~~/script-opts` directory
|
||||||
|
- `~/.config/mpv/script-opts` on Linux
|
||||||
|
- `%APPDATA%\mpv\script-opts` on Windows
|
||||||
|
to customize the script configuration as described in the next section
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
### Default Keybindings
|
||||||
|
|
||||||
|
- `add_to_queue - ctrl+a`: Add a video in the clipboard to the queue
|
||||||
|
- `download_current_video - ctrl+d`: Download the currently playing video
|
||||||
|
- `download_selected_video - ctrl+D`: Download the currently selected video
|
||||||
|
in the queue
|
||||||
|
- `move_cursor_down - ctrl+j`: Move the cursor down one row in the queue
|
||||||
|
- `move_cursor_up - ctrl+k`- Move the cursor up one row in the queue
|
||||||
|
- `load_queue - ctrl+l` - Appends the videos from the most recent save point to the
|
||||||
|
queue
|
||||||
|
- `move_video - ctrl+m`: Mark/move the selected video in the queue
|
||||||
|
- `play_next_in_queue - ctrl+n`: Play the next video in the queue
|
||||||
|
- `open_video_in_browser - ctrl+o`: Open the currently playing video in the browser
|
||||||
|
- `open_channel_in_browser - ctrl+O`: Open the channel page for the currently
|
||||||
|
playing video in the browser
|
||||||
|
- `play_previous_in_queue - ctrl+p`: Play the previous video in the queue
|
||||||
|
- `print_current_video - ctrl+P`: Print the name and channel of the currently
|
||||||
|
playing video to the OSD
|
||||||
|
- `print_queue - ctrl+q`: Print the contents of the queue to the OSD
|
||||||
|
- `save_queue - ctrl+s`: Saves the queue using the chosen method in
|
||||||
|
`default_save_method`
|
||||||
|
- `save_queue_alt - ctrl+S`: Saves the queue using the method not chosen in
|
||||||
|
`default_save_method`
|
||||||
|
- `remove_from_queue - ctrl+x`: Remove the currently selected video from the
|
||||||
|
queue
|
||||||
|
- `play_selected_video - ctrl+ENTER`: Play the currently selected video in
|
||||||
|
the queue
|
||||||
|
|
||||||
|
### Default Options
|
||||||
|
|
||||||
|
- `default_save_method - unwatched`: The default method to use when saving the
|
||||||
|
queue. Valid options are `unwatched` or `all`. Defaults to `unwatched`
|
||||||
|
- Whichever option is chosen is the default method for the `save_queue`
|
||||||
|
binding, and the other method will be bound to `save_queue_alt`
|
||||||
|
- `browser - firefox`: The browser to use when opening a video or channel page
|
||||||
|
- `clipboard_command - xclip -o`: The command to use to get the contents of the clipboard
|
||||||
|
- `cursor_icon - ➤`: The icon to use for the cursor
|
||||||
|
- `display_limit - 10`: The maximum amount of videos to show on the OSD at once
|
||||||
|
- `download_directory - ~/videos/YouTube`: The directory to use when downloading a video
|
||||||
|
- `download_quality 720p`: The maximum download quality
|
||||||
|
- `downloader - curl`: The name of the program to use to download the video
|
||||||
|
- `font_name - JetBrains Mono`: The name of the font to use
|
||||||
|
- `font_size - 12`: Size of the font
|
||||||
|
- `marked_icon - ⇅`: The icon to use to mark a video as ready to be moved in the queue
|
||||||
|
- `menu_timeout - 5`: The number of seconds until the menu times out
|
||||||
|
- `show_errors - yes`: Show error messages on the OSD
|
||||||
|
- `ytdlp_file_format - mp4`: The preferred file format for downloaded videos
|
||||||
|
- `ytdlp_output_template - %(uploader)s/%(title)s.%(ext)s`: The [yt-dlp output template string](https://github.com/yt-dlp/yt-dlp#output-template)
|
||||||
|
- Full path with the default `download_directory` is: `~/videos/YouTube/<uploader>/<title>.<ext>`
|
||||||
|
- `use_history_db - no`: Enable watch history tracking and remote video queuing through integration with [mpv-youtube-queue-server](https://gitea.suda.codes/sudacode/mpv-youtube-queue-server)
|
||||||
|
- `backend_host`: ip or hostname of the backend server
|
||||||
|
- `backend_port`: port to connect to for the backend server
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
This project is licensed under the terms of the GPLv3 license.
|
||||||
35
submodules/mpv-youtube-queue/mpv-youtube-queue.conf
Normal file
35
submodules/mpv-youtube-queue/mpv-youtube-queue.conf
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
add_to_queue=ctrl+a
|
||||||
|
default_save_method=unwatched
|
||||||
|
download_current_video=ctrl+d
|
||||||
|
download_selected_video=ctrl+D
|
||||||
|
move_cursor_down=ctrl+j
|
||||||
|
move_cursor_up=ctrl+k
|
||||||
|
load_queue=ctrl+l
|
||||||
|
move_video=ctrl+m
|
||||||
|
play_next_in_queue=ctrl+n
|
||||||
|
open_video_in_browser=ctrl+o
|
||||||
|
open_channel_in_browser=ctrl+O
|
||||||
|
play_previous_in_queue=ctrl+p
|
||||||
|
print_current_video=ctrl+P
|
||||||
|
print_queue=ctrl+q
|
||||||
|
save_queue=ctrl+s
|
||||||
|
save_full_alt=ctrl+S
|
||||||
|
remove_from_queue=ctrl+x
|
||||||
|
play_selected_video=ctrl+ENTER
|
||||||
|
browser=firefox
|
||||||
|
clipboard_command=xclip -o
|
||||||
|
cursor_icon=➤
|
||||||
|
display_limit=10
|
||||||
|
download_directory=~/videos/YouTube
|
||||||
|
download_quality=720p
|
||||||
|
downloader=curl
|
||||||
|
font_name=JetBrainsMono
|
||||||
|
font_size=12
|
||||||
|
marked_icon=⇅
|
||||||
|
menu_timeout=5
|
||||||
|
show_errors=yes
|
||||||
|
ytdlp_file_format=mp4
|
||||||
|
ytdlp_output_template=%(uploader)s/%(title)s.%(ext)s
|
||||||
|
use_history_db=no
|
||||||
|
backend_host=http://localhost
|
||||||
|
backend_port=42069
|
||||||
1041
submodules/mpv-youtube-queue/mpv-youtube-queue.lua
Normal file
1041
submodules/mpv-youtube-queue/mpv-youtube-queue.lua
Normal file
File diff suppressed because it is too large
Load Diff
58
submodules/mpv-youtube-upnext/.github/workflows/macos.yml
vendored
Normal file
58
submodules/mpv-youtube-upnext/.github/workflows/macos.yml
vendored
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
name: TestOnMacOs
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
push:
|
||||||
|
pull_request:
|
||||||
|
schedule:
|
||||||
|
- cron: "37 10 * * 3"
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
TestScript:
|
||||||
|
runs-on: macos-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout repo
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Get latest ytdl_hook.lua
|
||||||
|
run: |
|
||||||
|
wget https://github.com/mpv-player/mpv/raw/master/player/lua/ytdl_hook.lua
|
||||||
|
|
||||||
|
- name: Install mpv
|
||||||
|
run: |
|
||||||
|
brew install ffmpeg luajit mujs uchardet vapoursynth
|
||||||
|
brew install --ignore-dependencies mpv
|
||||||
|
env:
|
||||||
|
HOMEBREW_NO_INSTALL_CLEANUP: 1
|
||||||
|
HOMEBREW_NO_AUTO_UPDATE: 1
|
||||||
|
HOMEBREW_NO_INSTALLED_DEPENDENTS_CHECK: 1
|
||||||
|
|
||||||
|
- name: Test mpv
|
||||||
|
run: |
|
||||||
|
mpv
|
||||||
|
|
||||||
|
- name: Set up Python 3.11
|
||||||
|
uses: actions/setup-python@v4
|
||||||
|
with:
|
||||||
|
python-version: "3.11"
|
||||||
|
|
||||||
|
- name: Install yt-dlp from pip
|
||||||
|
run: |
|
||||||
|
pip install --upgrade yt-dlp
|
||||||
|
which yt-dlp
|
||||||
|
|
||||||
|
- name: Test yt-dlp
|
||||||
|
run: |
|
||||||
|
yt-dlp --version
|
||||||
|
|
||||||
|
- name: ⏯️ Run mpv with youtube-upnext userscript
|
||||||
|
run: |
|
||||||
|
ytloc=$(which yt-dlp)
|
||||||
|
output=$(mpv --ytdl=no --script-opts="ytdl_hook-ytdl_path=$ytloc,youtube-upnext-auto_add=no,youtube-upnext-fetch_on_start=yes,youtube-upnext-use_json_lua=yes" --scripts=ytdl_hook.lua:youtube-upnext.lua --msg-level=youtube_upnext=trace "https://www.youtube.com/watch?v=VpOLXXBcAl0" 2>&1)
|
||||||
|
echo $output
|
||||||
|
if [[ $output == *"downloaded and decoded json successfully"* ]]; then
|
||||||
|
exit 0
|
||||||
|
else
|
||||||
|
echo "Not found: downloaded and decoded json successfully"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
9
submodules/mpv-youtube-upnext/LICENSE
Normal file
9
submodules/mpv-youtube-upnext/LICENSE
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
LICENSE
|
||||||
|
|
||||||
|
The software is provided "as is" and the author disclaims all warranties with
|
||||||
|
regard to this software including all implied warranties of merchantability
|
||||||
|
and fitness. In no event shall the author be liable for any special, direct,
|
||||||
|
indirect, or consequential damages or any damages whatsoever resulting from
|
||||||
|
loss of use, data or profits, whether in an action of contract, negligence
|
||||||
|
or other tortious action, arising out of or in connection with the use or
|
||||||
|
performance of this software.
|
||||||
101
submodules/mpv-youtube-upnext/README.md
Normal file
101
submodules/mpv-youtube-upnext/README.md
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
# youtube-upnext
|
||||||
|
A userscript for MPV that allows you to load the up next/recommened youtube video on the fly, as though you were using the web player.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
Toggle the menu with `ctrl+u` (configurable). Select from the list with the arrow keys (configurable), and press `Enter` (configurable) to select. Menu times out after 10 seconds (configurable).
|
||||||
|
|
||||||
|
Press `Space` to append to the playlist. Close menu with `Escape` or `ctrl+u`.
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
Copy youtube-upnext.lua into your scripts directory, e.g.:
|
||||||
|
|
||||||
|
cp youtube-upnext.lua ~/.config/mpv/scripts/
|
||||||
|
|
||||||
|
optional, copy the config file:
|
||||||
|
|
||||||
|
cp youtube-upnext.conf ~/.config/mpv/script-opts/
|
||||||
|
|
||||||
|
|
||||||
|
Make sure you have cURL or GNU Wget installed.
|
||||||
|
cURL is included in Windows 10+, macOS, Android and many Linux distributions.
|
||||||
|
|
||||||
|
### Windows:
|
||||||
|
|
||||||
|
The location of the scripts directory on Windows is `%APPDATA%\mpv\scripts` e.g. `C:\Users\cvzi\AppData\Roaming\mpv\scripts`
|
||||||
|
|
||||||
|
$\textcolor{#D00000}{\textsf{Microsoft ships an old version of cURL with known vulnerabilities.}}$
|
||||||
|
You are encouraged to install a newer version of cURL:
|
||||||
|
* Official cURL releases https://curl.se/windows/
|
||||||
|
* [Chocolatey package](https://community.chocolatey.org/packages/curl)
|
||||||
|
* [git for windows](https://git-scm.com/) includes a version of cURL
|
||||||
|
|
||||||
|
If you downloaded/installed the curl/wget executable, you have to [add the directory of the curl.exe/wget.exe to your machine's
|
||||||
|
%PATH%](https://stackoverflow.com/a/41895179/10367381).
|
||||||
|
|
||||||
|
### [uosc](https://github.com/tomasklaen/uosc) menu integration
|
||||||
|
|
||||||
|
If you want to add the upnext menu to **uosc**, you need to add one of the following line to your `input.conf`.
|
||||||
|
If you use the line starting with `#` it just creates an entry in the uosc menu.
|
||||||
|
If you use the second line, it also creates a keyboard shortcut to open the menu.
|
||||||
|
Replace `u` with the key of your choice:
|
||||||
|
|
||||||
|
```
|
||||||
|
# script-message-to youtube_upnext menu #! Youtube Recommendations
|
||||||
|
OR
|
||||||
|
u script-message-to youtube_upnext menu #! Youtube Recommendations
|
||||||
|
```
|
||||||
|
|
||||||
|
If you want it to appear in a submenu, replace `Youtube Recommendations` with e.g. `Utils > Youtube Recommendations`
|
||||||
|
|
||||||
|
<ins>Note:</ins> If you're using the default menu of uosc and you don't have the uosc menu defined in `input.conf`, you first need to create
|
||||||
|
a menu configuration. You can find an example at https://github.com/tomasklaen/uosc#examples
|
||||||
|
|
||||||
|
If you want an entry in the controls bar of uosc, you can add the following to the `controls=` entry in your `uosc.conf`:
|
||||||
|
|
||||||
|
```
|
||||||
|
<stream>command:queue_play_next:script-message-to youtube_upnext menu?Up Next
|
||||||
|
```
|
||||||
|
|
||||||
|
* Pick your desired title instead of `Up Next`
|
||||||
|
* Pick another icon instead of `queue_play_next` from [Google Material Icons](https://fonts.google.com/icons?selected=Material+Icons)
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
### mpv.net:
|
||||||
|
The script folder for mpv.net is:
|
||||||
|
`%APPDATA%\mpv.net\scripts`
|
||||||
|
|
||||||
|
The .conf files belong into:
|
||||||
|
`%APPDATA%\mpv.net\script-opts`
|
||||||
|
|
||||||
|
The keyboard shortcut in the script and the .conf-file doesn't work with mpv.net.
|
||||||
|
You need to set the keyboard shortcut yourself in your `input.conf`. Default location is `%APPDATA%\mpv.net\input.conf`.
|
||||||
|
Add the following line to the end of your `input.conf` (change the key if it's already used):
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
Ctrl+u script-message-to youtube_upnext upnext-menu
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
### SVP
|
||||||
|
|
||||||
|
Installation is the same as for normal mpv. You need to install yt-dlp. On Windows you have to
|
||||||
|
[add the directory of the yt-dlp.exe to your machine's %PATH%](https://stackoverflow.com/a/41895179/10367381)
|
||||||
|
or put yt-dlp.exe into `%APPDATA%\mpv\` e.g. `C:\Users\cvzi\AppData\Roaming\mpv\yt-dlp.exe`.
|
||||||
|
|
||||||
|
You need to update yt-dlp occasionally when it stops working.
|
||||||
|
|
||||||
|
Unfortunately SVP does not make the quality available to scripts. That means the quality settings that were
|
||||||
|
selected in SVP are not preserved when the next video is loaded.
|
||||||
|
|
||||||
|
## Credit
|
||||||
|
- I pretty much copied the [mpv-youtube-quality](https://github.com/jgreco/mpv-youtube-quality) script and added wget
|
||||||
|
|
||||||
|
## [youtube-quality](https://github.com/jgreco/mpv-youtube-quality)'s Credit
|
||||||
|
- [reload.lua](https://github.com/4e6/mpv-reload/), for the function to reload a video while preserving the playlist.
|
||||||
|
- [mpv-playlistmanager](https://github.com/jonniek/mpv-playlistmanager), from which I ripped off much of the menu formatting config.
|
||||||
|
- ytdl_hook.lua, from which I ripped off much of the youtube-dl code to fetch the format list
|
||||||
|
- somebody on /mpv/ for the idea
|
||||||
BIN
submodules/mpv-youtube-upnext/screenshot_menu.png
Normal file
BIN
submodules/mpv-youtube-upnext/screenshot_menu.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 364 KiB |
BIN
submodules/mpv-youtube-upnext/screenshot_uosc.webp
Normal file
BIN
submodules/mpv-youtube-upnext/screenshot_uosc.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 168 KiB |
90
submodules/mpv-youtube-upnext/youtube-upnext.conf
Normal file
90
submodules/mpv-youtube-upnext/youtube-upnext.conf
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
# KEY BINDINGS
|
||||||
|
|
||||||
|
# invoke or dismiss the quality menu
|
||||||
|
toggle_menu_binding=ctrl+u
|
||||||
|
# move the menu cursor up
|
||||||
|
up_binding=UP
|
||||||
|
# move the menu cursor down
|
||||||
|
down_binding=DOWN
|
||||||
|
# select menu entry
|
||||||
|
select_binding=ENTER
|
||||||
|
|
||||||
|
# auto fetch recommended videos when opening a url
|
||||||
|
fetch_on_start=no
|
||||||
|
|
||||||
|
# auto load and add the "upnext" video to the playlist
|
||||||
|
auto_add=yes
|
||||||
|
|
||||||
|
# formatting / cursors
|
||||||
|
cursor_selected=●
|
||||||
|
cursor_unselected=○
|
||||||
|
cursor_appended=▷
|
||||||
|
cursor_appended_selected=▶
|
||||||
|
|
||||||
|
# font size scales by window, if false requires larger font and padding sizes
|
||||||
|
scale_playlist_by_window=no
|
||||||
|
|
||||||
|
# playlist ass style overrides inside curly brackets, \keyvalue is one field, extra \ for escape in lua
|
||||||
|
# example {\\fnUbuntu\\fs10\\b0\\bord1} equals: font=Ubuntu, size=10, bold=no, border=1
|
||||||
|
# read http://docs.aegisub.org/3.2/ASS_Tags/ for reference of tags
|
||||||
|
# undeclared tags will use default osd settings
|
||||||
|
# these styles will be used for the whole playlist. More specific styling will need to be hacked in
|
||||||
|
#
|
||||||
|
# (a monospaced font is recommended but not required)
|
||||||
|
style_ass_tags={\\fnmonospace\\fs25}
|
||||||
|
|
||||||
|
# paddings for top left corner
|
||||||
|
text_padding_x=5
|
||||||
|
text_padding_y=5
|
||||||
|
|
||||||
|
# Screen dim when menu is open 0.0 - 1.0 (0 is no dim, 1 is black)
|
||||||
|
curtain_opacity=0.7
|
||||||
|
|
||||||
|
# how many seconds until the quality menu times out
|
||||||
|
menu_timeout=10
|
||||||
|
|
||||||
|
# base url for loading new urls, %s will be replaced with video id
|
||||||
|
youtube_url=https://www.youtube.com/watch?v=%s
|
||||||
|
|
||||||
|
# Fallback Invidious instance. Used if "upnext" could not be retrieved from the normal youtube website
|
||||||
|
# See https://api.invidious.io/ for alternatives
|
||||||
|
invidious_instance=https://inv.tux.pizza
|
||||||
|
|
||||||
|
# Keep the width of the window the same when the next video is played
|
||||||
|
restore_window_width=no
|
||||||
|
|
||||||
|
# On Windows wget.exe may not be able to check SSL certificates for HTTPS, so you can disable it here
|
||||||
|
check_certificate=yes
|
||||||
|
|
||||||
|
|
||||||
|
# Use a cookies file
|
||||||
|
# Same as youtube-dl --cookies or wget --load-cookies
|
||||||
|
# If you don't set this, the script may create a cookie file for you
|
||||||
|
# For example "C:\\Users\\Username\\cookies.txt"
|
||||||
|
# Or "C:/Users/Username/cookies.txt"
|
||||||
|
#cookies=cookies.txt
|
||||||
|
|
||||||
|
# When a video is selected from the menu, the new video can be appended to the playlist
|
||||||
|
# or the playlist can be cleared and replaced with only the selected video.
|
||||||
|
# If yes, the video will be appended to the playlist. If no, the playlist will be cleared.
|
||||||
|
keep_playlist_on_select=yes
|
||||||
|
|
||||||
|
# What should happen if a video recommendation in uosc menu is clicked? Options are:
|
||||||
|
# submenu --> show a submenu with play/upnext/append option
|
||||||
|
# append --> append the video to the playlist
|
||||||
|
# insert --> play the video after the current video
|
||||||
|
# play --> append the video to the playlist and play it
|
||||||
|
# replace --> play the video and clear the playlist
|
||||||
|
uosc_entry_action=submenu
|
||||||
|
|
||||||
|
# Should the uosc menu stay open after clicking a video recommendation?
|
||||||
|
uosc_keep_menu_open=no
|
||||||
|
|
||||||
|
# Don't play/append videos that are shorter than this time. Format is "HH:MM:SS" or "MM:SS"
|
||||||
|
#skip_shorter_than=00:30
|
||||||
|
|
||||||
|
# Don't play/append videos that are longer than this time. Format is "HH:MM:SS" or "MM:SS"
|
||||||
|
#skip_longer_than=07:00
|
||||||
|
|
||||||
|
# Also don't show the videos in the menu that are too short or too long
|
||||||
|
#hide_skipped_videos=yes
|
||||||
1584
submodules/mpv-youtube-upnext/youtube-upnext.lua
Normal file
1584
submodules/mpv-youtube-upnext/youtube-upnext.lua
Normal file
File diff suppressed because it is too large
Load Diff
373
submodules/thumbfast/LICENSE
Normal file
373
submodules/thumbfast/LICENSE
Normal file
@@ -0,0 +1,373 @@
|
|||||||
|
Mozilla Public License Version 2.0
|
||||||
|
==================================
|
||||||
|
|
||||||
|
1. Definitions
|
||||||
|
--------------
|
||||||
|
|
||||||
|
1.1. "Contributor"
|
||||||
|
means each individual or legal entity that creates, contributes to
|
||||||
|
the creation of, or owns Covered Software.
|
||||||
|
|
||||||
|
1.2. "Contributor Version"
|
||||||
|
means the combination of the Contributions of others (if any) used
|
||||||
|
by a Contributor and that particular Contributor's Contribution.
|
||||||
|
|
||||||
|
1.3. "Contribution"
|
||||||
|
means Covered Software of a particular Contributor.
|
||||||
|
|
||||||
|
1.4. "Covered Software"
|
||||||
|
means Source Code Form to which the initial Contributor has attached
|
||||||
|
the notice in Exhibit A, the Executable Form of such Source Code
|
||||||
|
Form, and Modifications of such Source Code Form, in each case
|
||||||
|
including portions thereof.
|
||||||
|
|
||||||
|
1.5. "Incompatible With Secondary Licenses"
|
||||||
|
means
|
||||||
|
|
||||||
|
(a) that the initial Contributor has attached the notice described
|
||||||
|
in Exhibit B to the Covered Software; or
|
||||||
|
|
||||||
|
(b) that the Covered Software was made available under the terms of
|
||||||
|
version 1.1 or earlier of the License, but not also under the
|
||||||
|
terms of a Secondary License.
|
||||||
|
|
||||||
|
1.6. "Executable Form"
|
||||||
|
means any form of the work other than Source Code Form.
|
||||||
|
|
||||||
|
1.7. "Larger Work"
|
||||||
|
means a work that combines Covered Software with other material, in
|
||||||
|
a separate file or files, that is not Covered Software.
|
||||||
|
|
||||||
|
1.8. "License"
|
||||||
|
means this document.
|
||||||
|
|
||||||
|
1.9. "Licensable"
|
||||||
|
means having the right to grant, to the maximum extent possible,
|
||||||
|
whether at the time of the initial grant or subsequently, any and
|
||||||
|
all of the rights conveyed by this License.
|
||||||
|
|
||||||
|
1.10. "Modifications"
|
||||||
|
means any of the following:
|
||||||
|
|
||||||
|
(a) any file in Source Code Form that results from an addition to,
|
||||||
|
deletion from, or modification of the contents of Covered
|
||||||
|
Software; or
|
||||||
|
|
||||||
|
(b) any new file in Source Code Form that contains any Covered
|
||||||
|
Software.
|
||||||
|
|
||||||
|
1.11. "Patent Claims" of a Contributor
|
||||||
|
means any patent claim(s), including without limitation, method,
|
||||||
|
process, and apparatus claims, in any patent Licensable by such
|
||||||
|
Contributor that would be infringed, but for the grant of the
|
||||||
|
License, by the making, using, selling, offering for sale, having
|
||||||
|
made, import, or transfer of either its Contributions or its
|
||||||
|
Contributor Version.
|
||||||
|
|
||||||
|
1.12. "Secondary License"
|
||||||
|
means either the GNU General Public License, Version 2.0, the GNU
|
||||||
|
Lesser General Public License, Version 2.1, the GNU Affero General
|
||||||
|
Public License, Version 3.0, or any later versions of those
|
||||||
|
licenses.
|
||||||
|
|
||||||
|
1.13. "Source Code Form"
|
||||||
|
means the form of the work preferred for making modifications.
|
||||||
|
|
||||||
|
1.14. "You" (or "Your")
|
||||||
|
means an individual or a legal entity exercising rights under this
|
||||||
|
License. For legal entities, "You" includes any entity that
|
||||||
|
controls, is controlled by, or is under common control with You. For
|
||||||
|
purposes of this definition, "control" means (a) the power, direct
|
||||||
|
or indirect, to cause the direction or management of such entity,
|
||||||
|
whether by contract or otherwise, or (b) ownership of more than
|
||||||
|
fifty percent (50%) of the outstanding shares or beneficial
|
||||||
|
ownership of such entity.
|
||||||
|
|
||||||
|
2. License Grants and Conditions
|
||||||
|
--------------------------------
|
||||||
|
|
||||||
|
2.1. Grants
|
||||||
|
|
||||||
|
Each Contributor hereby grants You a world-wide, royalty-free,
|
||||||
|
non-exclusive license:
|
||||||
|
|
||||||
|
(a) under intellectual property rights (other than patent or trademark)
|
||||||
|
Licensable by such Contributor to use, reproduce, make available,
|
||||||
|
modify, display, perform, distribute, and otherwise exploit its
|
||||||
|
Contributions, either on an unmodified basis, with Modifications, or
|
||||||
|
as part of a Larger Work; and
|
||||||
|
|
||||||
|
(b) under Patent Claims of such Contributor to make, use, sell, offer
|
||||||
|
for sale, have made, import, and otherwise transfer either its
|
||||||
|
Contributions or its Contributor Version.
|
||||||
|
|
||||||
|
2.2. Effective Date
|
||||||
|
|
||||||
|
The licenses granted in Section 2.1 with respect to any Contribution
|
||||||
|
become effective for each Contribution on the date the Contributor first
|
||||||
|
distributes such Contribution.
|
||||||
|
|
||||||
|
2.3. Limitations on Grant Scope
|
||||||
|
|
||||||
|
The licenses granted in this Section 2 are the only rights granted under
|
||||||
|
this License. No additional rights or licenses will be implied from the
|
||||||
|
distribution or licensing of Covered Software under this License.
|
||||||
|
Notwithstanding Section 2.1(b) above, no patent license is granted by a
|
||||||
|
Contributor:
|
||||||
|
|
||||||
|
(a) for any code that a Contributor has removed from Covered Software;
|
||||||
|
or
|
||||||
|
|
||||||
|
(b) for infringements caused by: (i) Your and any other third party's
|
||||||
|
modifications of Covered Software, or (ii) the combination of its
|
||||||
|
Contributions with other software (except as part of its Contributor
|
||||||
|
Version); or
|
||||||
|
|
||||||
|
(c) under Patent Claims infringed by Covered Software in the absence of
|
||||||
|
its Contributions.
|
||||||
|
|
||||||
|
This License does not grant any rights in the trademarks, service marks,
|
||||||
|
or logos of any Contributor (except as may be necessary to comply with
|
||||||
|
the notice requirements in Section 3.4).
|
||||||
|
|
||||||
|
2.4. Subsequent Licenses
|
||||||
|
|
||||||
|
No Contributor makes additional grants as a result of Your choice to
|
||||||
|
distribute the Covered Software under a subsequent version of this
|
||||||
|
License (see Section 10.2) or under the terms of a Secondary License (if
|
||||||
|
permitted under the terms of Section 3.3).
|
||||||
|
|
||||||
|
2.5. Representation
|
||||||
|
|
||||||
|
Each Contributor represents that the Contributor believes its
|
||||||
|
Contributions are its original creation(s) or it has sufficient rights
|
||||||
|
to grant the rights to its Contributions conveyed by this License.
|
||||||
|
|
||||||
|
2.6. Fair Use
|
||||||
|
|
||||||
|
This License is not intended to limit any rights You have under
|
||||||
|
applicable copyright doctrines of fair use, fair dealing, or other
|
||||||
|
equivalents.
|
||||||
|
|
||||||
|
2.7. Conditions
|
||||||
|
|
||||||
|
Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
|
||||||
|
in Section 2.1.
|
||||||
|
|
||||||
|
3. Responsibilities
|
||||||
|
-------------------
|
||||||
|
|
||||||
|
3.1. Distribution of Source Form
|
||||||
|
|
||||||
|
All distribution of Covered Software in Source Code Form, including any
|
||||||
|
Modifications that You create or to which You contribute, must be under
|
||||||
|
the terms of this License. You must inform recipients that the Source
|
||||||
|
Code Form of the Covered Software is governed by the terms of this
|
||||||
|
License, and how they can obtain a copy of this License. You may not
|
||||||
|
attempt to alter or restrict the recipients' rights in the Source Code
|
||||||
|
Form.
|
||||||
|
|
||||||
|
3.2. Distribution of Executable Form
|
||||||
|
|
||||||
|
If You distribute Covered Software in Executable Form then:
|
||||||
|
|
||||||
|
(a) such Covered Software must also be made available in Source Code
|
||||||
|
Form, as described in Section 3.1, and You must inform recipients of
|
||||||
|
the Executable Form how they can obtain a copy of such Source Code
|
||||||
|
Form by reasonable means in a timely manner, at a charge no more
|
||||||
|
than the cost of distribution to the recipient; and
|
||||||
|
|
||||||
|
(b) You may distribute such Executable Form under the terms of this
|
||||||
|
License, or sublicense it under different terms, provided that the
|
||||||
|
license for the Executable Form does not attempt to limit or alter
|
||||||
|
the recipients' rights in the Source Code Form under this License.
|
||||||
|
|
||||||
|
3.3. Distribution of a Larger Work
|
||||||
|
|
||||||
|
You may create and distribute a Larger Work under terms of Your choice,
|
||||||
|
provided that You also comply with the requirements of this License for
|
||||||
|
the Covered Software. If the Larger Work is a combination of Covered
|
||||||
|
Software with a work governed by one or more Secondary Licenses, and the
|
||||||
|
Covered Software is not Incompatible With Secondary Licenses, this
|
||||||
|
License permits You to additionally distribute such Covered Software
|
||||||
|
under the terms of such Secondary License(s), so that the recipient of
|
||||||
|
the Larger Work may, at their option, further distribute the Covered
|
||||||
|
Software under the terms of either this License or such Secondary
|
||||||
|
License(s).
|
||||||
|
|
||||||
|
3.4. Notices
|
||||||
|
|
||||||
|
You may not remove or alter the substance of any license notices
|
||||||
|
(including copyright notices, patent notices, disclaimers of warranty,
|
||||||
|
or limitations of liability) contained within the Source Code Form of
|
||||||
|
the Covered Software, except that You may alter any license notices to
|
||||||
|
the extent required to remedy known factual inaccuracies.
|
||||||
|
|
||||||
|
3.5. Application of Additional Terms
|
||||||
|
|
||||||
|
You may choose to offer, and to charge a fee for, warranty, support,
|
||||||
|
indemnity or liability obligations to one or more recipients of Covered
|
||||||
|
Software. However, You may do so only on Your own behalf, and not on
|
||||||
|
behalf of any Contributor. You must make it absolutely clear that any
|
||||||
|
such warranty, support, indemnity, or liability obligation is offered by
|
||||||
|
You alone, and You hereby agree to indemnify every Contributor for any
|
||||||
|
liability incurred by such Contributor as a result of warranty, support,
|
||||||
|
indemnity or liability terms You offer. You may include additional
|
||||||
|
disclaimers of warranty and limitations of liability specific to any
|
||||||
|
jurisdiction.
|
||||||
|
|
||||||
|
4. Inability to Comply Due to Statute or Regulation
|
||||||
|
---------------------------------------------------
|
||||||
|
|
||||||
|
If it is impossible for You to comply with any of the terms of this
|
||||||
|
License with respect to some or all of the Covered Software due to
|
||||||
|
statute, judicial order, or regulation then You must: (a) comply with
|
||||||
|
the terms of this License to the maximum extent possible; and (b)
|
||||||
|
describe the limitations and the code they affect. Such description must
|
||||||
|
be placed in a text file included with all distributions of the Covered
|
||||||
|
Software under this License. Except to the extent prohibited by statute
|
||||||
|
or regulation, such description must be sufficiently detailed for a
|
||||||
|
recipient of ordinary skill to be able to understand it.
|
||||||
|
|
||||||
|
5. Termination
|
||||||
|
--------------
|
||||||
|
|
||||||
|
5.1. The rights granted under this License will terminate automatically
|
||||||
|
if You fail to comply with any of its terms. However, if You become
|
||||||
|
compliant, then the rights granted under this License from a particular
|
||||||
|
Contributor are reinstated (a) provisionally, unless and until such
|
||||||
|
Contributor explicitly and finally terminates Your grants, and (b) on an
|
||||||
|
ongoing basis, if such Contributor fails to notify You of the
|
||||||
|
non-compliance by some reasonable means prior to 60 days after You have
|
||||||
|
come back into compliance. Moreover, Your grants from a particular
|
||||||
|
Contributor are reinstated on an ongoing basis if such Contributor
|
||||||
|
notifies You of the non-compliance by some reasonable means, this is the
|
||||||
|
first time You have received notice of non-compliance with this License
|
||||||
|
from such Contributor, and You become compliant prior to 30 days after
|
||||||
|
Your receipt of the notice.
|
||||||
|
|
||||||
|
5.2. If You initiate litigation against any entity by asserting a patent
|
||||||
|
infringement claim (excluding declaratory judgment actions,
|
||||||
|
counter-claims, and cross-claims) alleging that a Contributor Version
|
||||||
|
directly or indirectly infringes any patent, then the rights granted to
|
||||||
|
You by any and all Contributors for the Covered Software under Section
|
||||||
|
2.1 of this License shall terminate.
|
||||||
|
|
||||||
|
5.3. In the event of termination under Sections 5.1 or 5.2 above, all
|
||||||
|
end user license agreements (excluding distributors and resellers) which
|
||||||
|
have been validly granted by You or Your distributors under this License
|
||||||
|
prior to termination shall survive termination.
|
||||||
|
|
||||||
|
************************************************************************
|
||||||
|
* *
|
||||||
|
* 6. Disclaimer of Warranty *
|
||||||
|
* ------------------------- *
|
||||||
|
* *
|
||||||
|
* Covered Software is provided under this License on an "as is" *
|
||||||
|
* basis, without warranty of any kind, either expressed, implied, or *
|
||||||
|
* statutory, including, without limitation, warranties that the *
|
||||||
|
* Covered Software is free of defects, merchantable, fit for a *
|
||||||
|
* particular purpose or non-infringing. The entire risk as to the *
|
||||||
|
* quality and performance of the Covered Software is with You. *
|
||||||
|
* Should any Covered Software prove defective in any respect, You *
|
||||||
|
* (not any Contributor) assume the cost of any necessary servicing, *
|
||||||
|
* repair, or correction. This disclaimer of warranty constitutes an *
|
||||||
|
* essential part of this License. No use of any Covered Software is *
|
||||||
|
* authorized under this License except under this disclaimer. *
|
||||||
|
* *
|
||||||
|
************************************************************************
|
||||||
|
|
||||||
|
************************************************************************
|
||||||
|
* *
|
||||||
|
* 7. Limitation of Liability *
|
||||||
|
* -------------------------- *
|
||||||
|
* *
|
||||||
|
* Under no circumstances and under no legal theory, whether tort *
|
||||||
|
* (including negligence), contract, or otherwise, shall any *
|
||||||
|
* Contributor, or anyone who distributes Covered Software as *
|
||||||
|
* permitted above, be liable to You for any direct, indirect, *
|
||||||
|
* special, incidental, or consequential damages of any character *
|
||||||
|
* including, without limitation, damages for lost profits, loss of *
|
||||||
|
* goodwill, work stoppage, computer failure or malfunction, or any *
|
||||||
|
* and all other commercial damages or losses, even if such party *
|
||||||
|
* shall have been informed of the possibility of such damages. This *
|
||||||
|
* limitation of liability shall not apply to liability for death or *
|
||||||
|
* personal injury resulting from such party's negligence to the *
|
||||||
|
* extent applicable law prohibits such limitation. Some *
|
||||||
|
* jurisdictions do not allow the exclusion or limitation of *
|
||||||
|
* incidental or consequential damages, so this exclusion and *
|
||||||
|
* limitation may not apply to You. *
|
||||||
|
* *
|
||||||
|
************************************************************************
|
||||||
|
|
||||||
|
8. Litigation
|
||||||
|
-------------
|
||||||
|
|
||||||
|
Any litigation relating to this License may be brought only in the
|
||||||
|
courts of a jurisdiction where the defendant maintains its principal
|
||||||
|
place of business and such litigation shall be governed by laws of that
|
||||||
|
jurisdiction, without reference to its conflict-of-law provisions.
|
||||||
|
Nothing in this Section shall prevent a party's ability to bring
|
||||||
|
cross-claims or counter-claims.
|
||||||
|
|
||||||
|
9. Miscellaneous
|
||||||
|
----------------
|
||||||
|
|
||||||
|
This License represents the complete agreement concerning the subject
|
||||||
|
matter hereof. If any provision of this License is held to be
|
||||||
|
unenforceable, such provision shall be reformed only to the extent
|
||||||
|
necessary to make it enforceable. Any law or regulation which provides
|
||||||
|
that the language of a contract shall be construed against the drafter
|
||||||
|
shall not be used to construe this License against a Contributor.
|
||||||
|
|
||||||
|
10. Versions of the License
|
||||||
|
---------------------------
|
||||||
|
|
||||||
|
10.1. New Versions
|
||||||
|
|
||||||
|
Mozilla Foundation is the license steward. Except as provided in Section
|
||||||
|
10.3, no one other than the license steward has the right to modify or
|
||||||
|
publish new versions of this License. Each version will be given a
|
||||||
|
distinguishing version number.
|
||||||
|
|
||||||
|
10.2. Effect of New Versions
|
||||||
|
|
||||||
|
You may distribute the Covered Software under the terms of the version
|
||||||
|
of the License under which You originally received the Covered Software,
|
||||||
|
or under the terms of any subsequent version published by the license
|
||||||
|
steward.
|
||||||
|
|
||||||
|
10.3. Modified Versions
|
||||||
|
|
||||||
|
If you create software not governed by this License, and you want to
|
||||||
|
create a new license for such software, you may create and use a
|
||||||
|
modified version of this License if you rename the license and remove
|
||||||
|
any references to the name of the license steward (except to note that
|
||||||
|
such modified license differs from this License).
|
||||||
|
|
||||||
|
10.4. Distributing Source Code Form that is Incompatible With Secondary
|
||||||
|
Licenses
|
||||||
|
|
||||||
|
If You choose to distribute Source Code Form that is Incompatible With
|
||||||
|
Secondary Licenses under the terms of this version of the License, the
|
||||||
|
notice described in Exhibit B of this License must be attached.
|
||||||
|
|
||||||
|
Exhibit A - Source Code Form License Notice
|
||||||
|
-------------------------------------------
|
||||||
|
|
||||||
|
This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||||
|
|
||||||
|
If it is not possible or desirable to put the notice in a particular
|
||||||
|
file, then You may include the notice in a location (such as a LICENSE
|
||||||
|
file in a relevant directory) where a recipient would be likely to look
|
||||||
|
for such a notice.
|
||||||
|
|
||||||
|
You may add additional accurate notices of copyright ownership.
|
||||||
|
|
||||||
|
Exhibit B - "Incompatible With Secondary Licenses" Notice
|
||||||
|
---------------------------------------------------------
|
||||||
|
|
||||||
|
This Source Code Form is "Incompatible With Secondary Licenses", as
|
||||||
|
defined by the Mozilla Public License, v. 2.0.
|
||||||
126
submodules/thumbfast/README.md
Normal file
126
submodules/thumbfast/README.md
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
# thumbfast
|
||||||
|
High-performance on-the-fly thumbnailer for mpv.
|
||||||
|
|
||||||
|
**The script does not display thumbnails on its own,** it is meant to be used alongside a UI script that calls thumbfast.
|
||||||
|
|
||||||
|
[Preview of thumbfast on different UIs](https://user-images.githubusercontent.com/42466980/199102896-65f9e989-4189-4734-82a7-bda8ee63c7a6.webm)
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
Place thumbfast.lua in your mpv `scripts` folder.
|
||||||
|
Default settings are listed in thumbfast.conf, copy it to your mpv `script-opts` folder to customize.
|
||||||
|
|
||||||
|
For the vanilla UI, you also have to install [osc.lua](https://github.com/po5/thumbfast/blob/vanilla-osc/player/lua/osc.lua) (identical to the mpv default, with added thumbfast support) into your `scripts` folder.
|
||||||
|
For third-party UIs, refer to their respective installation instructions. [See the list of supported UIs.](#ui-support)
|
||||||
|
|
||||||
|
## Features
|
||||||
|
No dependencies, no background thumbnail generation hogging your CPU.
|
||||||
|
Customizable sizes, interval between thumbnails, cropping support, respects applied video filters.
|
||||||
|
Supports web videos e.g. YouTube (disabled by default), mixed aspect ratio videos.
|
||||||
|
|
||||||
|
This script makes an effort to run on mpv versions as old as 0.29.0 (Windows, Linux) and 0.33.0 (Mac).
|
||||||
|
Note that most custom UIs will not support vintage mpv builds, update before submitting an issue and mention if behavior is the same.
|
||||||
|
Support for <0.33.0 on Linux requires socat.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
Once the lua file is in your scripts directory, and you are using a UI that supports thumbfast, you are done.
|
||||||
|
Hover on the timeline for nice thumbnails.
|
||||||
|
|
||||||
|
## UI support
|
||||||
|
- [uosc](https://github.com/tomasklaen/uosc)
|
||||||
|
- [osc.lua](https://github.com/po5/thumbfast/blob/vanilla-osc/player/lua/osc.lua) (use this fork for vanilla UI)
|
||||||
|
- [progressbar](https://github.com/torque/mpv-progressbar)
|
||||||
|
- [tethys](https://github.com/Zren/mpv-osc-tethys) (PR pending, [lua](https://github.com/po5/mpv-osc-tethys/blob/thumbfast/osc_tethys.lua))
|
||||||
|
- [modern](https://github.com/maoiscat/mpv-osc-modern/tree/with.thumbfast) (separate branch)
|
||||||
|
- [ModernX](https://github.com/cyl0/ModernX)
|
||||||
|
- [oscc](https://github.com/longtermfree/oscc)
|
||||||
|
- [mfpbar](https://codeberg.org/NRK/mpv-toolbox/src/branch/master/mfpbar)
|
||||||
|
|
||||||
|
## mpv frontends
|
||||||
|
[ImPlay](https://tsl0922.github.io/ImPlay/) is auto-detected, but if you encounter issues set `mpv_path=ImPlay` in `script-opts/thumbfast.conf`.
|
||||||
|
|
||||||
|
[mpv.net](https://github.com/mpvnet-player/mpv.net) is directly supported since v7, no special configuration is required.
|
||||||
|
|
||||||
|
Other frontends and older versions of mpv.net will need [standalone mpv](https://mpv.io/installation/) accessible within [Path](https://learn.microsoft.com/en-us/previous-versions/office/developer/sharepoint-2010/ee537574(v=office.14)#to-add-a-path-to-the-path-environment-variable).
|
||||||
|
The easiest way is to copy standalone mpv files inside of your frontend's installation folder.
|
||||||
|
It will be used in the background to generate thumbnails.
|
||||||
|
|
||||||
|
## MacOS
|
||||||
|
If your mpv install is an app bundle (e.g. stolendata builds), the script will work but you may notice the Dock shakes when generating the first thumbnail.
|
||||||
|
To get rid of the shaking, make sure the app is in your Applications folder, then run: `sudo ln -s /Applications/mpv.app/Contents/MacOS/mpv /usr/local/mpv`
|
||||||
|
If you installed mpv via [Homebrew](https://brew.sh/), there are no issues.
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
`socket`: On Windows, a plain string. On Linux and Mac, a directory path for temporary files. Leave empty for auto.
|
||||||
|
`thumbnail`: Path for the temporary thumbnail file (must not be a directory). Leave empty for auto.
|
||||||
|
`max_height`, `max_width`: Maximum thumbnail generation size in pixels (scaled down to fit). Values are scaled when hidpi is enabled. Defaults to 200x200.
|
||||||
|
`scale_factor`: Scale factor for thumbnail display size (requires mpv 0.38+). Lower quality than increasing max_height and max_width. Defaults to 1.
|
||||||
|
`tone_mapping`: Apply tone-mapping, no to disable. Defaults to auto, which copies your mpv config.
|
||||||
|
`overlay_id`: Overlay id for thumbnails. Leave blank unless you know what you're doing.
|
||||||
|
`spawn_first`: Spawn thumbnailer on file load for faster initial thumbnails. Defaults to no.
|
||||||
|
`quit_after_inactivity`: Close thumbnailer process after an inactivity period in seconds. Defaults to 0 (disabled).
|
||||||
|
`network`: Enable on remote files. Defaults to no.
|
||||||
|
`audio`: Enable on audio files. Defaults to no.
|
||||||
|
`hwdec`: Enable hardware decoding. Defaults to no.
|
||||||
|
`direct_io`: Windows only: write directly to pipe (requires LuaJIT). Should improve performance, ymmv.
|
||||||
|
`mpv_path`: Custom path to the mpv executable. Defaults to mpv.
|
||||||
|
|
||||||
|
## For UI developers: How to add thumbfast support to your script
|
||||||
|
This API usage example code is [CC0 (public domain)](https://creativecommons.org/share-your-work/public-domain/cc0/).
|
||||||
|
|
||||||
|
Declare the thumbfast state variable near the top of your script.
|
||||||
|
*Do not manually modify those values, they are automatically updated by the script and changes will be overwritten.*
|
||||||
|
```lua
|
||||||
|
local thumbfast = {
|
||||||
|
width = 0,
|
||||||
|
height = 0,
|
||||||
|
disabled = true,
|
||||||
|
available = false
|
||||||
|
}
|
||||||
|
```
|
||||||
|
Register the state setter near the end of your script, or near where your other script messages are.
|
||||||
|
You are expected to have required `mp.utils` (for this example, into a `utils` variable).
|
||||||
|
```lua
|
||||||
|
mp.register_script_message("thumbfast-info", function(json)
|
||||||
|
local data = utils.parse_json(json)
|
||||||
|
if type(data) ~= "table" or not data.width or not data.height then
|
||||||
|
msg.error("thumbfast-info: received json didn't produce a table with thumbnail information")
|
||||||
|
else
|
||||||
|
thumbfast = data
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
```
|
||||||
|
Now for the actual functionality. You are in charge of supplying the time hovered (in seconds), and x/y coordinates for the top-left corner of the thumbnail.
|
||||||
|
In this example, the thumbnail is horizontally centered on the cursor, respects a 10px margin on both sides, and displays 10px above the cursor.
|
||||||
|
This code should be run when the user hovers on the seekbar. Don't worry even if this is called on every render, thumbfast won't be bogged down.
|
||||||
|
```lua
|
||||||
|
-- below are examples of what these values may look like
|
||||||
|
-- margin_left = 10
|
||||||
|
-- margin_right = 10
|
||||||
|
-- cursor_x, cursor_y = mp.get_mouse_pos()
|
||||||
|
-- display_width = mp.get_property_number("osd-width")
|
||||||
|
-- hovered_seconds = video_duration * cursor_x / display_width
|
||||||
|
|
||||||
|
if not thumbfast.disabled then
|
||||||
|
mp.commandv("script-message-to", "thumbfast", "thumb",
|
||||||
|
-- hovered time in seconds
|
||||||
|
hovered_seconds,
|
||||||
|
-- x
|
||||||
|
math.min(display_width - thumbfast.width - margin_right, math.max(margin_left, cursor_x - thumbfast.width / 2)),
|
||||||
|
-- y
|
||||||
|
cursor_y - 10 - thumbfast.height
|
||||||
|
)
|
||||||
|
end
|
||||||
|
```
|
||||||
|
This code should be run when the user leaves the seekbar.
|
||||||
|
```lua
|
||||||
|
if thumbfast.available then
|
||||||
|
mp.commandv("script-message-to", "thumbfast", "clear")
|
||||||
|
end
|
||||||
|
```
|
||||||
|
If you did all that, your script can now display thumbnails!
|
||||||
|
Look at existing integrations for more concrete examples.
|
||||||
|
|
||||||
|
If positioning isn't enough and you want complete control over rendering:
|
||||||
|
Register a `thumbfast-render` script message.
|
||||||
|
When requesting the thumbnail, set x and y to empty strings and supply your script's name as the 4th argument.
|
||||||
|
You will recieve a json object with the keys `width`, `height`, `x`, `y`, `socket`, `thumbnail`, `overlay_id` when the thumbnail is ready.
|
||||||
41
submodules/thumbfast/thumbfast.conf
Normal file
41
submodules/thumbfast/thumbfast.conf
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
# Socket path (leave empty for auto)
|
||||||
|
socket=
|
||||||
|
|
||||||
|
# Thumbnail path (leave empty for auto)
|
||||||
|
thumbnail=
|
||||||
|
|
||||||
|
# Maximum thumbnail generation size in pixels (scaled down to fit)
|
||||||
|
# Values are scaled when hidpi is enabled
|
||||||
|
max_height=200
|
||||||
|
max_width=200
|
||||||
|
|
||||||
|
# Scale factor for thumbnail display size (requires mpv 0.38+)
|
||||||
|
# Note that this is lower quality than increasing max_height and max_width
|
||||||
|
scale_factor=1
|
||||||
|
|
||||||
|
# Apply tone-mapping, no to disable
|
||||||
|
tone_mapping=auto
|
||||||
|
|
||||||
|
# Overlay id
|
||||||
|
overlay_id=42
|
||||||
|
|
||||||
|
# Spawn thumbnailer on file load for faster initial thumbnails
|
||||||
|
spawn_first=no
|
||||||
|
|
||||||
|
# Close thumbnailer process after an inactivity period in seconds, 0 to disable
|
||||||
|
quit_after_inactivity=0
|
||||||
|
|
||||||
|
# Enable on network playback
|
||||||
|
network=no
|
||||||
|
|
||||||
|
# Enable on audio playback
|
||||||
|
audio=no
|
||||||
|
|
||||||
|
# Enable hardware decoding
|
||||||
|
hwdec=no
|
||||||
|
|
||||||
|
# Windows only: use native Windows API to write to pipe (requires LuaJIT)
|
||||||
|
direct_io=no
|
||||||
|
|
||||||
|
# Custom path to the mpv executable
|
||||||
|
mpv_path=mpv
|
||||||
951
submodules/thumbfast/thumbfast.lua
Normal file
951
submodules/thumbfast/thumbfast.lua
Normal file
@@ -0,0 +1,951 @@
|
|||||||
|
-- thumbfast.lua
|
||||||
|
--
|
||||||
|
-- High-performance on-the-fly thumbnailer
|
||||||
|
--
|
||||||
|
-- Built for easy integration in third-party UIs.
|
||||||
|
|
||||||
|
--[[
|
||||||
|
This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||||
|
]]
|
||||||
|
|
||||||
|
local options = {
|
||||||
|
-- Socket path (leave empty for auto)
|
||||||
|
socket = "",
|
||||||
|
|
||||||
|
-- Thumbnail path (leave empty for auto)
|
||||||
|
thumbnail = "",
|
||||||
|
|
||||||
|
-- Maximum thumbnail generation size in pixels (scaled down to fit)
|
||||||
|
-- Values are scaled when hidpi is enabled
|
||||||
|
max_height = 200,
|
||||||
|
max_width = 200,
|
||||||
|
|
||||||
|
-- Scale factor for thumbnail display size (requires mpv 0.38+)
|
||||||
|
-- Note that this is lower quality than increasing max_height and max_width
|
||||||
|
scale_factor = 1,
|
||||||
|
|
||||||
|
-- Apply tone-mapping, no to disable
|
||||||
|
tone_mapping = "auto",
|
||||||
|
|
||||||
|
-- Overlay id
|
||||||
|
overlay_id = 42,
|
||||||
|
|
||||||
|
-- Spawn thumbnailer on file load for faster initial thumbnails
|
||||||
|
spawn_first = false,
|
||||||
|
|
||||||
|
-- Close thumbnailer process after an inactivity period in seconds, 0 to disable
|
||||||
|
quit_after_inactivity = 0,
|
||||||
|
|
||||||
|
-- Enable on network playback
|
||||||
|
network = false,
|
||||||
|
|
||||||
|
-- Enable on audio playback
|
||||||
|
audio = false,
|
||||||
|
|
||||||
|
-- Enable hardware decoding
|
||||||
|
hwdec = false,
|
||||||
|
|
||||||
|
-- Windows only: use native Windows API to write to pipe (requires LuaJIT)
|
||||||
|
direct_io = false,
|
||||||
|
|
||||||
|
-- Custom path to the mpv executable
|
||||||
|
mpv_path = "mpv"
|
||||||
|
}
|
||||||
|
|
||||||
|
mp.utils = require "mp.utils"
|
||||||
|
mp.options = require "mp.options"
|
||||||
|
mp.options.read_options(options, "thumbfast")
|
||||||
|
|
||||||
|
local properties = {}
|
||||||
|
local pre_0_30_0 = mp.command_native_async == nil
|
||||||
|
local pre_0_33_0 = true
|
||||||
|
local support_media_control = mp.get_property_native("media-controls") ~= nil
|
||||||
|
|
||||||
|
function subprocess(args, async, callback)
|
||||||
|
callback = callback or function() end
|
||||||
|
|
||||||
|
if not pre_0_30_0 then
|
||||||
|
if async then
|
||||||
|
return mp.command_native_async({name = "subprocess", playback_only = true, args = args, env = "PATH="..os.getenv("PATH")}, callback)
|
||||||
|
else
|
||||||
|
return mp.command_native({name = "subprocess", playback_only = false, capture_stdout = true, args = args, env = "PATH="..os.getenv("PATH")})
|
||||||
|
end
|
||||||
|
else
|
||||||
|
if async then
|
||||||
|
return mp.utils.subprocess_detached({args = args}, callback)
|
||||||
|
else
|
||||||
|
return mp.utils.subprocess({args = args})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local winapi = {}
|
||||||
|
if options.direct_io then
|
||||||
|
local ffi_loaded, ffi = pcall(require, "ffi")
|
||||||
|
if ffi_loaded then
|
||||||
|
winapi = {
|
||||||
|
ffi = ffi,
|
||||||
|
C = ffi.C,
|
||||||
|
bit = require("bit"),
|
||||||
|
socket_wc = "",
|
||||||
|
|
||||||
|
-- WinAPI constants
|
||||||
|
CP_UTF8 = 65001,
|
||||||
|
GENERIC_WRITE = 0x40000000,
|
||||||
|
OPEN_EXISTING = 3,
|
||||||
|
FILE_FLAG_WRITE_THROUGH = 0x80000000,
|
||||||
|
FILE_FLAG_NO_BUFFERING = 0x20000000,
|
||||||
|
PIPE_NOWAIT = ffi.new("unsigned long[1]", 0x00000001),
|
||||||
|
|
||||||
|
INVALID_HANDLE_VALUE = ffi.cast("void*", -1),
|
||||||
|
|
||||||
|
-- don't care about how many bytes WriteFile wrote, so allocate something to store the result once
|
||||||
|
_lpNumberOfBytesWritten = ffi.new("unsigned long[1]"),
|
||||||
|
}
|
||||||
|
-- cache flags used in run() to avoid bor() call
|
||||||
|
winapi._createfile_pipe_flags = winapi.bit.bor(winapi.FILE_FLAG_WRITE_THROUGH, winapi.FILE_FLAG_NO_BUFFERING)
|
||||||
|
|
||||||
|
ffi.cdef[[
|
||||||
|
void* __stdcall CreateFileW(const wchar_t *lpFileName, unsigned long dwDesiredAccess, unsigned long dwShareMode, void *lpSecurityAttributes, unsigned long dwCreationDisposition, unsigned long dwFlagsAndAttributes, void *hTemplateFile);
|
||||||
|
bool __stdcall WriteFile(void *hFile, const void *lpBuffer, unsigned long nNumberOfBytesToWrite, unsigned long *lpNumberOfBytesWritten, void *lpOverlapped);
|
||||||
|
bool __stdcall CloseHandle(void *hObject);
|
||||||
|
bool __stdcall SetNamedPipeHandleState(void *hNamedPipe, unsigned long *lpMode, unsigned long *lpMaxCollectionCount, unsigned long *lpCollectDataTimeout);
|
||||||
|
int __stdcall MultiByteToWideChar(unsigned int CodePage, unsigned long dwFlags, const char *lpMultiByteStr, int cbMultiByte, wchar_t *lpWideCharStr, int cchWideChar);
|
||||||
|
]]
|
||||||
|
|
||||||
|
winapi.MultiByteToWideChar = function(MultiByteStr)
|
||||||
|
if MultiByteStr then
|
||||||
|
local utf16_len = winapi.C.MultiByteToWideChar(winapi.CP_UTF8, 0, MultiByteStr, -1, nil, 0)
|
||||||
|
if utf16_len > 0 then
|
||||||
|
local utf16_str = winapi.ffi.new("wchar_t[?]", utf16_len)
|
||||||
|
if winapi.C.MultiByteToWideChar(winapi.CP_UTF8, 0, MultiByteStr, -1, utf16_str, utf16_len) > 0 then
|
||||||
|
return utf16_str
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return ""
|
||||||
|
end
|
||||||
|
|
||||||
|
else
|
||||||
|
options.direct_io = false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local file
|
||||||
|
local file_bytes = 0
|
||||||
|
local spawned = false
|
||||||
|
local disabled = false
|
||||||
|
local force_disabled = false
|
||||||
|
local spawn_waiting = false
|
||||||
|
local spawn_working = false
|
||||||
|
local script_written = false
|
||||||
|
|
||||||
|
local dirty = false
|
||||||
|
|
||||||
|
local x, y
|
||||||
|
local last_x, last_y
|
||||||
|
|
||||||
|
local last_seek_time
|
||||||
|
|
||||||
|
local effective_w, effective_h = options.max_width, options.max_height
|
||||||
|
local real_w, real_h
|
||||||
|
local last_real_w, last_real_h
|
||||||
|
|
||||||
|
local script_name
|
||||||
|
|
||||||
|
local show_thumbnail = false
|
||||||
|
|
||||||
|
local filters_reset = {["lavfi-crop"]=true, ["crop"]=true}
|
||||||
|
local filters_runtime = {["hflip"]=true, ["vflip"]=true}
|
||||||
|
local filters_all = {["hflip"]=true, ["vflip"]=true, ["lavfi-crop"]=true, ["crop"]=true}
|
||||||
|
|
||||||
|
local tone_mappings = {["none"]=true, ["clip"]=true, ["linear"]=true, ["gamma"]=true, ["reinhard"]=true, ["hable"]=true, ["mobius"]=true}
|
||||||
|
local last_tone_mapping
|
||||||
|
|
||||||
|
local last_vf_reset = ""
|
||||||
|
local last_vf_runtime = ""
|
||||||
|
|
||||||
|
local last_rotate = 0
|
||||||
|
|
||||||
|
local par = ""
|
||||||
|
local last_par = ""
|
||||||
|
|
||||||
|
local last_crop = nil
|
||||||
|
|
||||||
|
local last_has_vid = 0
|
||||||
|
local has_vid = 0
|
||||||
|
|
||||||
|
local file_timer
|
||||||
|
local file_check_period = 1/60
|
||||||
|
|
||||||
|
local allow_fast_seek = true
|
||||||
|
|
||||||
|
local client_script = [=[
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
MPV_IPC_FD=0; MPV_IPC_PATH="%s"
|
||||||
|
trap "kill 0" EXIT
|
||||||
|
while [[ $# -ne 0 ]]; do case $1 in --mpv-ipc-fd=*) MPV_IPC_FD=${1/--mpv-ipc-fd=/} ;; esac; shift; done
|
||||||
|
if echo "print-text thumbfast" >&"$MPV_IPC_FD"; then echo -n > "$MPV_IPC_PATH"; tail -f "$MPV_IPC_PATH" >&"$MPV_IPC_FD" & while read -r -u "$MPV_IPC_FD" 2>/dev/null; do :; done; fi
|
||||||
|
]=]
|
||||||
|
|
||||||
|
local function get_os()
|
||||||
|
local raw_os_name = ""
|
||||||
|
|
||||||
|
if jit and jit.os and jit.arch then
|
||||||
|
raw_os_name = jit.os
|
||||||
|
else
|
||||||
|
if package.config:sub(1,1) == "\\" then
|
||||||
|
-- Windows
|
||||||
|
local env_OS = os.getenv("OS")
|
||||||
|
if env_OS then
|
||||||
|
raw_os_name = env_OS
|
||||||
|
end
|
||||||
|
else
|
||||||
|
raw_os_name = subprocess({"uname", "-s"}).stdout
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
raw_os_name = (raw_os_name):lower()
|
||||||
|
|
||||||
|
local os_patterns = {
|
||||||
|
["windows"] = "windows",
|
||||||
|
["linux"] = "linux",
|
||||||
|
|
||||||
|
["osx"] = "darwin",
|
||||||
|
["mac"] = "darwin",
|
||||||
|
["darwin"] = "darwin",
|
||||||
|
|
||||||
|
["^mingw"] = "windows",
|
||||||
|
["^cygwin"] = "windows",
|
||||||
|
|
||||||
|
["bsd$"] = "darwin",
|
||||||
|
["sunos"] = "darwin"
|
||||||
|
}
|
||||||
|
|
||||||
|
-- Default to linux
|
||||||
|
local str_os_name = "linux"
|
||||||
|
|
||||||
|
for pattern, name in pairs(os_patterns) do
|
||||||
|
if raw_os_name:match(pattern) then
|
||||||
|
str_os_name = name
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return str_os_name
|
||||||
|
end
|
||||||
|
|
||||||
|
local os_name = mp.get_property("platform") or get_os()
|
||||||
|
|
||||||
|
local path_separator = os_name == "windows" and "\\" or "/"
|
||||||
|
|
||||||
|
if options.socket == "" then
|
||||||
|
if os_name == "windows" then
|
||||||
|
options.socket = "thumbfast"
|
||||||
|
else
|
||||||
|
options.socket = "/tmp/thumbfast"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if options.thumbnail == "" then
|
||||||
|
if os_name == "windows" then
|
||||||
|
options.thumbnail = os.getenv("TEMP").."\\thumbfast.out"
|
||||||
|
else
|
||||||
|
options.thumbnail = "/tmp/thumbfast.out"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local unique = mp.utils.getpid()
|
||||||
|
|
||||||
|
options.socket = options.socket .. unique
|
||||||
|
options.thumbnail = options.thumbnail .. unique
|
||||||
|
|
||||||
|
if options.direct_io then
|
||||||
|
if os_name == "windows" then
|
||||||
|
winapi.socket_wc = winapi.MultiByteToWideChar("\\\\.\\pipe\\" .. options.socket)
|
||||||
|
end
|
||||||
|
|
||||||
|
if winapi.socket_wc == "" then
|
||||||
|
options.direct_io = false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
options.scale_factor = math.floor(options.scale_factor)
|
||||||
|
|
||||||
|
local mpv_path = options.mpv_path
|
||||||
|
local frontend_path
|
||||||
|
|
||||||
|
if mpv_path == "mpv" and os_name == "windows" then
|
||||||
|
frontend_path = mp.get_property_native("user-data/frontend/process-path")
|
||||||
|
mpv_path = frontend_path or mpv_path
|
||||||
|
end
|
||||||
|
|
||||||
|
if mpv_path == "mpv" and os_name == "darwin" and unique then
|
||||||
|
-- TODO: look into ~~osxbundle/
|
||||||
|
mpv_path = string.gsub(subprocess({"ps", "-o", "comm=", "-p", tostring(unique)}).stdout, "[\n\r]", "")
|
||||||
|
if mpv_path ~= "mpv" then
|
||||||
|
mpv_path = string.gsub(mpv_path, "/mpv%-bundle$", "/mpv")
|
||||||
|
local mpv_bin = mp.utils.file_info("/usr/local/mpv")
|
||||||
|
if mpv_bin and mpv_bin.is_file then
|
||||||
|
mpv_path = "/usr/local/mpv"
|
||||||
|
else
|
||||||
|
local mpv_app = mp.utils.file_info("/Applications/mpv.app/Contents/MacOS/mpv")
|
||||||
|
if mpv_app and mpv_app.is_file then
|
||||||
|
mp.msg.warn("symlink mpv to fix Dock icons: `sudo ln -s /Applications/mpv.app/Contents/MacOS/mpv /usr/local/mpv`")
|
||||||
|
else
|
||||||
|
mp.msg.warn("drag to your Applications folder and symlink mpv to fix Dock icons: `sudo ln -s /Applications/mpv.app/Contents/MacOS/mpv /usr/local/mpv`")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function vo_tone_mapping()
|
||||||
|
local passes = mp.get_property_native("vo-passes")
|
||||||
|
if passes and passes["fresh"] then
|
||||||
|
for k, v in pairs(passes["fresh"]) do
|
||||||
|
for k2, v2 in pairs(v) do
|
||||||
|
if k2 == "desc" and v2 then
|
||||||
|
local tone_mapping = string.match(v2, "([0-9a-z.-]+) tone map")
|
||||||
|
if tone_mapping then
|
||||||
|
return tone_mapping
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function vf_string(filters, full)
|
||||||
|
local vf = ""
|
||||||
|
local vf_table = properties["vf"]
|
||||||
|
|
||||||
|
if (properties["video-crop"] or "") ~= "" then
|
||||||
|
vf = "lavfi-crop="..string.gsub(properties["video-crop"], "(%d*)x?(%d*)%+(%d+)%+(%d+)", "w=%1:h=%2:x=%3:y=%4")..","
|
||||||
|
local width = properties["video-out-params"] and properties["video-out-params"]["dw"]
|
||||||
|
local height = properties["video-out-params"] and properties["video-out-params"]["dh"]
|
||||||
|
if width and height then
|
||||||
|
vf = string.gsub(vf, "w=:h=:", "w="..width..":h="..height..":")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if vf_table and #vf_table > 0 then
|
||||||
|
for i = #vf_table, 1, -1 do
|
||||||
|
if filters[vf_table[i].name] then
|
||||||
|
local args = ""
|
||||||
|
for key, value in pairs(vf_table[i].params) do
|
||||||
|
if args ~= "" then
|
||||||
|
args = args .. ":"
|
||||||
|
end
|
||||||
|
args = args .. key .. "=" .. value
|
||||||
|
end
|
||||||
|
vf = vf .. vf_table[i].name .. "=" .. args .. ","
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if (full and options.tone_mapping ~= "no") or options.tone_mapping == "auto" then
|
||||||
|
if properties["video-params"] and properties["video-params"]["primaries"] == "bt.2020" then
|
||||||
|
local tone_mapping = options.tone_mapping
|
||||||
|
if tone_mapping == "auto" then
|
||||||
|
tone_mapping = last_tone_mapping or properties["tone-mapping"]
|
||||||
|
if tone_mapping == "auto" and properties["current-vo"] == "gpu-next" then
|
||||||
|
tone_mapping = vo_tone_mapping()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if not tone_mappings[tone_mapping] then
|
||||||
|
tone_mapping = "hable"
|
||||||
|
end
|
||||||
|
last_tone_mapping = tone_mapping
|
||||||
|
vf = vf .. "zscale=transfer=linear,format=gbrpf32le,tonemap="..tone_mapping..",zscale=transfer=bt709,"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if full then
|
||||||
|
vf = vf.."scale=w="..effective_w..":h="..effective_h..par..",pad=w="..effective_w..":h="..effective_h..":x=-1:y=-1,format=bgra"
|
||||||
|
end
|
||||||
|
|
||||||
|
return vf
|
||||||
|
end
|
||||||
|
|
||||||
|
local function calc_dimensions()
|
||||||
|
local width = properties["video-out-params"] and properties["video-out-params"]["dw"]
|
||||||
|
local height = properties["video-out-params"] and properties["video-out-params"]["dh"]
|
||||||
|
if not width or not height then return end
|
||||||
|
|
||||||
|
local scale = properties["display-hidpi-scale"] or 1
|
||||||
|
|
||||||
|
if width / height > options.max_width / options.max_height then
|
||||||
|
effective_w = math.floor(options.max_width * scale + 0.5)
|
||||||
|
effective_h = math.floor(height / width * effective_w + 0.5)
|
||||||
|
else
|
||||||
|
effective_h = math.floor(options.max_height * scale + 0.5)
|
||||||
|
effective_w = math.floor(width / height * effective_h + 0.5)
|
||||||
|
end
|
||||||
|
|
||||||
|
local v_par = properties["video-out-params"] and properties["video-out-params"]["par"] or 1
|
||||||
|
if v_par == 1 then
|
||||||
|
par = ":force_original_aspect_ratio=decrease"
|
||||||
|
else
|
||||||
|
par = ""
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local info_timer = nil
|
||||||
|
|
||||||
|
local function info(w, h)
|
||||||
|
local rotate = properties["video-params"] and properties["video-params"]["rotate"]
|
||||||
|
local image = properties["current-tracks/video"] and properties["current-tracks/video"]["image"]
|
||||||
|
local albumart = image and properties["current-tracks/video"]["albumart"]
|
||||||
|
|
||||||
|
disabled = (w or 0) == 0 or (h or 0) == 0 or
|
||||||
|
has_vid == 0 or
|
||||||
|
(properties["demuxer-via-network"] and not options.network) or
|
||||||
|
(albumart and not options.audio) or
|
||||||
|
(image and not albumart) or
|
||||||
|
force_disabled
|
||||||
|
|
||||||
|
if info_timer then
|
||||||
|
info_timer:kill()
|
||||||
|
info_timer = nil
|
||||||
|
elseif has_vid == 0 or (rotate == nil and not disabled) then
|
||||||
|
info_timer = mp.add_timeout(0.05, function() info(w, h) end)
|
||||||
|
end
|
||||||
|
|
||||||
|
local json, err = mp.utils.format_json({width=w * options.scale_factor, height=h * options.scale_factor, scale_factor=options.scale_factor, disabled=disabled, available=true, socket=options.socket, thumbnail=options.thumbnail, overlay_id=options.overlay_id})
|
||||||
|
if pre_0_30_0 then
|
||||||
|
mp.command_native({"script-message", "thumbfast-info", json})
|
||||||
|
else
|
||||||
|
mp.command_native_async({"script-message", "thumbfast-info", json}, function() end)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function remove_thumbnail_files()
|
||||||
|
if file then
|
||||||
|
file:close()
|
||||||
|
file = nil
|
||||||
|
file_bytes = 0
|
||||||
|
end
|
||||||
|
os.remove(options.thumbnail)
|
||||||
|
os.remove(options.thumbnail..".bgra")
|
||||||
|
end
|
||||||
|
|
||||||
|
local activity_timer
|
||||||
|
|
||||||
|
local function spawn(time)
|
||||||
|
if disabled then return end
|
||||||
|
|
||||||
|
local path = properties["path"]
|
||||||
|
if path == nil then return end
|
||||||
|
|
||||||
|
if options.quit_after_inactivity > 0 then
|
||||||
|
if show_thumbnail or activity_timer:is_enabled() then
|
||||||
|
activity_timer:kill()
|
||||||
|
end
|
||||||
|
activity_timer:resume()
|
||||||
|
end
|
||||||
|
|
||||||
|
local open_filename = properties["stream-open-filename"]
|
||||||
|
local ytdl = open_filename and properties["demuxer-via-network"] and path ~= open_filename
|
||||||
|
if ytdl then
|
||||||
|
path = open_filename
|
||||||
|
end
|
||||||
|
|
||||||
|
remove_thumbnail_files()
|
||||||
|
|
||||||
|
local vid = properties["vid"]
|
||||||
|
has_vid = vid or 0
|
||||||
|
|
||||||
|
local args = {
|
||||||
|
mpv_path, "--no-config", "--msg-level=all=no", "--idle", "--pause", "--keep-open=always", "--really-quiet", "--no-terminal",
|
||||||
|
"--load-scripts=no", "--osc=no", "--ytdl=no", "--load-stats-overlay=no", "--load-osd-console=no", "--load-auto-profiles=no",
|
||||||
|
"--edition="..(properties["edition"] or "auto"), "--vid="..(vid or "auto"), "--no-sub", "--no-audio",
|
||||||
|
"--start="..time, allow_fast_seek and "--hr-seek=no" or "--hr-seek=yes",
|
||||||
|
"--ytdl-format=worst", "--demuxer-readahead-secs=0", "--demuxer-max-bytes=128KiB",
|
||||||
|
"--vd-lavc-skiploopfilter=all", "--vd-lavc-software-fallback=1", "--vd-lavc-fast", "--vd-lavc-threads=2", "--hwdec="..(options.hwdec and "auto" or "no"),
|
||||||
|
"--vf="..vf_string(filters_all, true),
|
||||||
|
"--sws-scaler=fast-bilinear",
|
||||||
|
"--video-rotate="..last_rotate,
|
||||||
|
"--ovc=rawvideo", "--of=image2", "--ofopts=update=1", "--o="..options.thumbnail
|
||||||
|
}
|
||||||
|
|
||||||
|
if not pre_0_30_0 then
|
||||||
|
table.insert(args, "--sws-allow-zimg=no")
|
||||||
|
end
|
||||||
|
|
||||||
|
if support_media_control then
|
||||||
|
table.insert(args, "--media-controls=no")
|
||||||
|
end
|
||||||
|
|
||||||
|
if os_name == "darwin" and properties["macos-app-activation-policy"] then
|
||||||
|
table.insert(args, "--macos-app-activation-policy=accessory")
|
||||||
|
end
|
||||||
|
|
||||||
|
if os_name == "windows" or pre_0_33_0 then
|
||||||
|
table.insert(args, "--input-ipc-server="..options.socket)
|
||||||
|
elseif not script_written then
|
||||||
|
local client_script_path = options.socket..".run"
|
||||||
|
local script = io.open(client_script_path, "w+")
|
||||||
|
if script == nil then
|
||||||
|
mp.msg.error("client script write failed")
|
||||||
|
return
|
||||||
|
else
|
||||||
|
script_written = true
|
||||||
|
script:write(string.format(client_script, options.socket))
|
||||||
|
script:close()
|
||||||
|
subprocess({"chmod", "+x", client_script_path}, true)
|
||||||
|
table.insert(args, "--scripts="..client_script_path)
|
||||||
|
end
|
||||||
|
else
|
||||||
|
local client_script_path = options.socket..".run"
|
||||||
|
table.insert(args, "--scripts="..client_script_path)
|
||||||
|
end
|
||||||
|
|
||||||
|
table.insert(args, "--")
|
||||||
|
table.insert(args, path)
|
||||||
|
|
||||||
|
spawned = true
|
||||||
|
spawn_waiting = true
|
||||||
|
|
||||||
|
subprocess(args, true,
|
||||||
|
function(success, result)
|
||||||
|
if spawn_waiting and (success == false or (result.status ~= 0 and result.status ~= -2)) then
|
||||||
|
spawned = false
|
||||||
|
spawn_waiting = false
|
||||||
|
options.tone_mapping = "no"
|
||||||
|
mp.msg.error("mpv subprocess create failed")
|
||||||
|
if not spawn_working then -- notify users of required configuration
|
||||||
|
if options.mpv_path == "mpv" then
|
||||||
|
if properties["current-vo"] == "libmpv" then
|
||||||
|
if options.mpv_path == mpv_path then -- attempt to locate ImPlay
|
||||||
|
mpv_path = "ImPlay"
|
||||||
|
spawn(time)
|
||||||
|
else -- ImPlay not in path
|
||||||
|
if os_name ~= "darwin" then
|
||||||
|
force_disabled = true
|
||||||
|
info(real_w or effective_w, real_h or effective_h)
|
||||||
|
end
|
||||||
|
mp.commandv("show-text", "thumbfast: ERROR! cannot create mpv subprocess", 5000)
|
||||||
|
mp.commandv("script-message-to", "implay", "show-message", "thumbfast initial setup", "Set mpv_path=PATH_TO_ImPlay in thumbfast config:\n" .. string.gsub(mp.command_native({"expand-path", "~~/script-opts/thumbfast.conf"}), "[/\\]", path_separator).."\nand restart ImPlay")
|
||||||
|
end
|
||||||
|
else
|
||||||
|
mp.commandv("show-text", "thumbfast: ERROR! cannot create mpv subprocess", 5000)
|
||||||
|
if os_name == "windows" and frontend_path == nil then
|
||||||
|
mp.commandv("script-message-to", "mpvnet", "show-text", "thumbfast: ERROR! install standalone mpv, see README", 5000, 20)
|
||||||
|
mp.commandv("script-message", "mpv.net", "show-text", "thumbfast: ERROR! install standalone mpv, see README", 5000, 20)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
else
|
||||||
|
mp.commandv("show-text", "thumbfast: ERROR! cannot create mpv subprocess", 5000)
|
||||||
|
-- found ImPlay but not defined in config
|
||||||
|
mp.commandv("script-message-to", "implay", "show-message", "thumbfast", "Set mpv_path=PATH_TO_ImPlay in thumbfast config:\n" .. string.gsub(mp.command_native({"expand-path", "~~/script-opts/thumbfast.conf"}), "[/\\]", path_separator).."\nand restart ImPlay")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
elseif success == true and (result.status == 0 or result.status == -2) then
|
||||||
|
if not spawn_working and properties["current-vo"] == "libmpv" and options.mpv_path ~= mpv_path then
|
||||||
|
mp.commandv("script-message-to", "implay", "show-message", "thumbfast initial setup", "Set mpv_path=ImPlay in thumbfast config:\n" .. string.gsub(mp.command_native({"expand-path", "~~/script-opts/thumbfast.conf"}), "[/\\]", path_separator).."\nand restart ImPlay")
|
||||||
|
end
|
||||||
|
spawn_working = true
|
||||||
|
spawn_waiting = false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function run(command)
|
||||||
|
if not spawned then return end
|
||||||
|
|
||||||
|
if options.direct_io then
|
||||||
|
local hPipe = winapi.C.CreateFileW(winapi.socket_wc, winapi.GENERIC_WRITE, 0, nil, winapi.OPEN_EXISTING, winapi._createfile_pipe_flags, nil)
|
||||||
|
if hPipe ~= winapi.INVALID_HANDLE_VALUE then
|
||||||
|
local buf = command .. "\n"
|
||||||
|
winapi.C.SetNamedPipeHandleState(hPipe, winapi.PIPE_NOWAIT, nil, nil)
|
||||||
|
winapi.C.WriteFile(hPipe, buf, #buf + 1, winapi._lpNumberOfBytesWritten, nil)
|
||||||
|
winapi.C.CloseHandle(hPipe)
|
||||||
|
end
|
||||||
|
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
local command_n = command.."\n"
|
||||||
|
|
||||||
|
if os_name == "windows" then
|
||||||
|
if file and file_bytes + #command_n >= 4096 then
|
||||||
|
file:close()
|
||||||
|
file = nil
|
||||||
|
file_bytes = 0
|
||||||
|
end
|
||||||
|
if not file then
|
||||||
|
file = io.open("\\\\.\\pipe\\"..options.socket, "r+b")
|
||||||
|
end
|
||||||
|
elseif pre_0_33_0 then
|
||||||
|
subprocess({"/usr/bin/env", "sh", "-c", "echo '" .. command .. "' | socat - " .. options.socket})
|
||||||
|
return
|
||||||
|
elseif not file then
|
||||||
|
file = io.open(options.socket, "r+")
|
||||||
|
end
|
||||||
|
if file then
|
||||||
|
file_bytes = file:seek("end")
|
||||||
|
file:write(command_n)
|
||||||
|
file:flush()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function draw(w, h, script)
|
||||||
|
if not w or not show_thumbnail then return end
|
||||||
|
if x ~= nil then
|
||||||
|
local scale_w, scale_h = options.scale_factor ~= 1 and (w * options.scale_factor) or nil, options.scale_factor ~= 1 and (h * options.scale_factor) or nil
|
||||||
|
if pre_0_30_0 then
|
||||||
|
mp.command_native({"overlay-add", options.overlay_id, x, y, options.thumbnail..".bgra", 0, "bgra", w, h, (4*w), scale_w, scale_h})
|
||||||
|
else
|
||||||
|
mp.command_native_async({"overlay-add", options.overlay_id, x, y, options.thumbnail..".bgra", 0, "bgra", w, h, (4*w), scale_w, scale_h}, function() end)
|
||||||
|
end
|
||||||
|
elseif script then
|
||||||
|
local json, err = mp.utils.format_json({width=w, height=h, scale_factor=options.scale_factor, x=x, y=y, socket=options.socket, thumbnail=options.thumbnail, overlay_id=options.overlay_id})
|
||||||
|
mp.commandv("script-message-to", script, "thumbfast-render", json)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function real_res(req_w, req_h, filesize)
|
||||||
|
local count = filesize / 4
|
||||||
|
local diff = (req_w * req_h) - count
|
||||||
|
|
||||||
|
if (properties["video-params"] and properties["video-params"]["rotate"] or 0) % 180 == 90 then
|
||||||
|
req_w, req_h = req_h, req_w
|
||||||
|
end
|
||||||
|
|
||||||
|
if diff == 0 then
|
||||||
|
return req_w, req_h
|
||||||
|
else
|
||||||
|
local threshold = 5 -- throw out results that change too much
|
||||||
|
local long_side, short_side = req_w, req_h
|
||||||
|
if req_h > req_w then
|
||||||
|
long_side, short_side = req_h, req_w
|
||||||
|
end
|
||||||
|
for a = short_side, short_side - threshold, -1 do
|
||||||
|
if count % a == 0 then
|
||||||
|
local b = count / a
|
||||||
|
if long_side - b < threshold then
|
||||||
|
if req_h < req_w then return b, a else return a, b end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function move_file(from, to)
|
||||||
|
if os_name == "windows" then
|
||||||
|
os.remove(to)
|
||||||
|
end
|
||||||
|
-- move the file because it can get overwritten while overlay-add is reading it, and crash the player
|
||||||
|
os.rename(from, to)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function seek(fast)
|
||||||
|
if last_seek_time then
|
||||||
|
run("async seek " .. last_seek_time .. (fast and " absolute+keyframes" or " absolute+exact"))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local seek_period = 3/60
|
||||||
|
local seek_period_counter = 0
|
||||||
|
local seek_timer
|
||||||
|
seek_timer = mp.add_periodic_timer(seek_period, function()
|
||||||
|
if seek_period_counter == 0 then
|
||||||
|
seek(allow_fast_seek)
|
||||||
|
seek_period_counter = 1
|
||||||
|
else
|
||||||
|
if seek_period_counter == 2 then
|
||||||
|
if allow_fast_seek then
|
||||||
|
seek_timer:kill()
|
||||||
|
seek()
|
||||||
|
end
|
||||||
|
else seek_period_counter = seek_period_counter + 1 end
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
seek_timer:kill()
|
||||||
|
|
||||||
|
local function request_seek()
|
||||||
|
if seek_timer:is_enabled() then
|
||||||
|
seek_period_counter = 0
|
||||||
|
else
|
||||||
|
seek_timer:resume()
|
||||||
|
seek(allow_fast_seek)
|
||||||
|
seek_period_counter = 1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function check_new_thumb()
|
||||||
|
-- the slave might start writing to the file after checking existance and
|
||||||
|
-- validity but before actually moving the file, so move to a temporary
|
||||||
|
-- location before validity check to make sure everything stays consistant
|
||||||
|
-- and valid thumbnails don't get overwritten by invalid ones
|
||||||
|
local tmp = options.thumbnail..".tmp"
|
||||||
|
move_file(options.thumbnail, tmp)
|
||||||
|
local finfo = mp.utils.file_info(tmp)
|
||||||
|
if not finfo then return false end
|
||||||
|
spawn_waiting = false
|
||||||
|
local w, h = real_res(effective_w, effective_h, finfo.size)
|
||||||
|
if w then -- only accept valid thumbnails
|
||||||
|
move_file(tmp, options.thumbnail..".bgra")
|
||||||
|
|
||||||
|
real_w, real_h = w, h
|
||||||
|
if real_w and (real_w ~= last_real_w or real_h ~= last_real_h) then
|
||||||
|
last_real_w, last_real_h = real_w, real_h
|
||||||
|
info(real_w, real_h)
|
||||||
|
end
|
||||||
|
if not show_thumbnail then
|
||||||
|
file_timer:kill()
|
||||||
|
end
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
file_timer = mp.add_periodic_timer(file_check_period, function()
|
||||||
|
if check_new_thumb() then
|
||||||
|
draw(real_w, real_h, script_name)
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
file_timer:kill()
|
||||||
|
|
||||||
|
local function clear()
|
||||||
|
file_timer:kill()
|
||||||
|
seek_timer:kill()
|
||||||
|
if options.quit_after_inactivity > 0 then
|
||||||
|
if show_thumbnail or activity_timer:is_enabled() then
|
||||||
|
activity_timer:kill()
|
||||||
|
end
|
||||||
|
activity_timer:resume()
|
||||||
|
end
|
||||||
|
last_seek_time = nil
|
||||||
|
show_thumbnail = false
|
||||||
|
last_x = nil
|
||||||
|
last_y = nil
|
||||||
|
if script_name then return end
|
||||||
|
if pre_0_30_0 then
|
||||||
|
mp.command_native({"overlay-remove", options.overlay_id})
|
||||||
|
else
|
||||||
|
mp.command_native_async({"overlay-remove", options.overlay_id}, function() end)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function quit()
|
||||||
|
activity_timer:kill()
|
||||||
|
if show_thumbnail then
|
||||||
|
activity_timer:resume()
|
||||||
|
return
|
||||||
|
end
|
||||||
|
run("quit")
|
||||||
|
spawned = false
|
||||||
|
real_w, real_h = nil, nil
|
||||||
|
clear()
|
||||||
|
end
|
||||||
|
|
||||||
|
activity_timer = mp.add_timeout(options.quit_after_inactivity, quit)
|
||||||
|
activity_timer:kill()
|
||||||
|
|
||||||
|
local function thumb(time, r_x, r_y, script)
|
||||||
|
if disabled then return end
|
||||||
|
|
||||||
|
time = tonumber(time)
|
||||||
|
if time == nil then return end
|
||||||
|
|
||||||
|
if r_x == "" or r_y == "" then
|
||||||
|
x, y = nil, nil
|
||||||
|
else
|
||||||
|
x, y = math.floor(r_x + 0.5), math.floor(r_y + 0.5)
|
||||||
|
end
|
||||||
|
|
||||||
|
script_name = script
|
||||||
|
if last_x ~= x or last_y ~= y or not show_thumbnail then
|
||||||
|
show_thumbnail = true
|
||||||
|
last_x, last_y = x, y
|
||||||
|
draw(real_w, real_h, script)
|
||||||
|
end
|
||||||
|
|
||||||
|
if options.quit_after_inactivity > 0 then
|
||||||
|
if show_thumbnail or activity_timer:is_enabled() then
|
||||||
|
activity_timer:kill()
|
||||||
|
end
|
||||||
|
activity_timer:resume()
|
||||||
|
end
|
||||||
|
|
||||||
|
if time == last_seek_time then return end
|
||||||
|
last_seek_time = time
|
||||||
|
if not spawned then spawn(time) end
|
||||||
|
request_seek()
|
||||||
|
if not file_timer:is_enabled() then file_timer:resume() end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function watch_changes()
|
||||||
|
if not dirty or not properties["video-out-params"] then return end
|
||||||
|
dirty = false
|
||||||
|
|
||||||
|
local old_w = effective_w
|
||||||
|
local old_h = effective_h
|
||||||
|
|
||||||
|
calc_dimensions()
|
||||||
|
|
||||||
|
local vf_reset = vf_string(filters_reset)
|
||||||
|
local rotate = properties["video-rotate"] or 0
|
||||||
|
|
||||||
|
local resized = old_w ~= effective_w or
|
||||||
|
old_h ~= effective_h or
|
||||||
|
last_vf_reset ~= vf_reset or
|
||||||
|
(last_rotate % 180) ~= (rotate % 180) or
|
||||||
|
par ~= last_par or last_crop ~= properties["video-crop"]
|
||||||
|
|
||||||
|
if resized then
|
||||||
|
last_rotate = rotate
|
||||||
|
info(effective_w, effective_h)
|
||||||
|
elseif last_has_vid ~= has_vid and has_vid ~= 0 then
|
||||||
|
info(effective_w, effective_h)
|
||||||
|
end
|
||||||
|
|
||||||
|
if spawned then
|
||||||
|
if resized then
|
||||||
|
-- mpv doesn't allow us to change output size
|
||||||
|
local seek_time = last_seek_time
|
||||||
|
run("quit")
|
||||||
|
clear()
|
||||||
|
spawned = false
|
||||||
|
spawn(seek_time or mp.get_property_number("time-pos", 0))
|
||||||
|
file_timer:resume()
|
||||||
|
else
|
||||||
|
if rotate ~= last_rotate then
|
||||||
|
run("set video-rotate "..rotate)
|
||||||
|
end
|
||||||
|
local vf_runtime = vf_string(filters_runtime)
|
||||||
|
if vf_runtime ~= last_vf_runtime then
|
||||||
|
run("vf set "..vf_string(filters_all, true))
|
||||||
|
last_vf_runtime = vf_runtime
|
||||||
|
end
|
||||||
|
end
|
||||||
|
else
|
||||||
|
last_vf_runtime = vf_string(filters_runtime)
|
||||||
|
end
|
||||||
|
|
||||||
|
last_vf_reset = vf_reset
|
||||||
|
last_rotate = rotate
|
||||||
|
last_par = par
|
||||||
|
last_crop = properties["video-crop"]
|
||||||
|
last_has_vid = has_vid
|
||||||
|
|
||||||
|
if not spawned and not disabled and options.spawn_first and resized then
|
||||||
|
spawn(mp.get_property_number("time-pos", 0))
|
||||||
|
file_timer:resume()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function update_property(name, value)
|
||||||
|
properties[name] = value
|
||||||
|
end
|
||||||
|
|
||||||
|
local function update_property_dirty(name, value)
|
||||||
|
properties[name] = value
|
||||||
|
dirty = true
|
||||||
|
if name == "tone-mapping" then
|
||||||
|
last_tone_mapping = nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function update_tracklist(name, value)
|
||||||
|
-- current-tracks shim
|
||||||
|
for _, track in ipairs(value) do
|
||||||
|
if track.type == "video" and track.selected then
|
||||||
|
properties["current-tracks/video"] = track
|
||||||
|
return
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function sync_changes(prop, val)
|
||||||
|
update_property(prop, val)
|
||||||
|
if val == nil then return end
|
||||||
|
|
||||||
|
if type(val) == "boolean" then
|
||||||
|
if prop == "vid" then
|
||||||
|
has_vid = 0
|
||||||
|
last_has_vid = 0
|
||||||
|
info(effective_w, effective_h)
|
||||||
|
clear()
|
||||||
|
return
|
||||||
|
end
|
||||||
|
val = val and "yes" or "no"
|
||||||
|
end
|
||||||
|
|
||||||
|
if prop == "vid" then
|
||||||
|
has_vid = 1
|
||||||
|
end
|
||||||
|
|
||||||
|
if not spawned then return end
|
||||||
|
|
||||||
|
run("set "..prop.." "..val)
|
||||||
|
dirty = true
|
||||||
|
end
|
||||||
|
|
||||||
|
local function file_load()
|
||||||
|
clear()
|
||||||
|
spawned = false
|
||||||
|
real_w, real_h = nil, nil
|
||||||
|
last_real_w, last_real_h = nil, nil
|
||||||
|
last_tone_mapping = nil
|
||||||
|
last_seek_time = nil
|
||||||
|
if info_timer then
|
||||||
|
info_timer:kill()
|
||||||
|
info_timer = nil
|
||||||
|
end
|
||||||
|
|
||||||
|
calc_dimensions()
|
||||||
|
info(effective_w, effective_h)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function shutdown()
|
||||||
|
run("quit")
|
||||||
|
remove_thumbnail_files()
|
||||||
|
if os_name ~= "windows" then
|
||||||
|
os.remove(options.socket)
|
||||||
|
os.remove(options.socket..".run")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function on_duration(prop, val)
|
||||||
|
allow_fast_seek = (val or 30) >= 30
|
||||||
|
end
|
||||||
|
|
||||||
|
mp.observe_property("current-tracks/video", "native", function(name, value)
|
||||||
|
if pre_0_33_0 then
|
||||||
|
mp.unobserve_property(update_tracklist)
|
||||||
|
pre_0_33_0 = false
|
||||||
|
end
|
||||||
|
update_property(name, value)
|
||||||
|
end)
|
||||||
|
|
||||||
|
mp.observe_property("track-list", "native", update_tracklist)
|
||||||
|
mp.observe_property("display-hidpi-scale", "native", update_property_dirty)
|
||||||
|
mp.observe_property("video-out-params", "native", update_property_dirty)
|
||||||
|
mp.observe_property("video-params", "native", update_property_dirty)
|
||||||
|
mp.observe_property("vf", "native", update_property_dirty)
|
||||||
|
mp.observe_property("tone-mapping", "native", update_property_dirty)
|
||||||
|
mp.observe_property("demuxer-via-network", "native", update_property)
|
||||||
|
mp.observe_property("stream-open-filename", "native", update_property)
|
||||||
|
mp.observe_property("macos-app-activation-policy", "native", update_property)
|
||||||
|
mp.observe_property("current-vo", "native", update_property)
|
||||||
|
mp.observe_property("video-rotate", "native", update_property)
|
||||||
|
mp.observe_property("video-crop", "native", update_property)
|
||||||
|
mp.observe_property("path", "native", update_property)
|
||||||
|
mp.observe_property("vid", "native", sync_changes)
|
||||||
|
mp.observe_property("edition", "native", sync_changes)
|
||||||
|
mp.observe_property("duration", "native", on_duration)
|
||||||
|
|
||||||
|
mp.register_script_message("thumb", thumb)
|
||||||
|
mp.register_script_message("clear", clear)
|
||||||
|
|
||||||
|
mp.register_event("file-loaded", file_load)
|
||||||
|
mp.register_event("shutdown", shutdown)
|
||||||
|
|
||||||
|
mp.register_idle(watch_changes)
|
||||||
405
submodules/ytdl-preload/ytdl-preload.lua
Normal file
405
submodules/ytdl-preload/ytdl-preload.lua
Normal file
@@ -0,0 +1,405 @@
|
|||||||
|
----------------------
|
||||||
|
-- #example ytdl_preload.conf
|
||||||
|
-- # make sure lines do not have trailing whitespace
|
||||||
|
-- # ytdl_opt has no sanity check and should be formatted exactly how it would appear in yt-dlp CLI, they are split into a key/value pair on whitespace
|
||||||
|
-- # at least on Windows, do not escape '\' in temp, just us a single one for each divider
|
||||||
|
|
||||||
|
-- #temp=R:\ytdltest
|
||||||
|
-- #ytdl_opt1=-r 50k
|
||||||
|
-- #ytdl_opt2=-N 5
|
||||||
|
-- #ytdl_opt#=etc
|
||||||
|
----------------------
|
||||||
|
local nextIndex
|
||||||
|
local caught = true
|
||||||
|
-- local pop = false
|
||||||
|
local ytdl = "yt-dlp"
|
||||||
|
local utils = require 'mp.utils'
|
||||||
|
|
||||||
|
local options = require 'mp.options'
|
||||||
|
local opts = {
|
||||||
|
temp = "R:\\ytdl",
|
||||||
|
ytdl_opt1 = "",
|
||||||
|
ytdl_opt2 = "",
|
||||||
|
ytdl_opt3 = "",
|
||||||
|
ytdl_opt4 = "",
|
||||||
|
ytdl_opt5 = "",
|
||||||
|
ytdl_opt6 = "",
|
||||||
|
ytdl_opt7 = "",
|
||||||
|
ytdl_opt8 = "",
|
||||||
|
ytdl_opt9 = "",
|
||||||
|
}
|
||||||
|
options.read_options(opts, "ytdl_preload")
|
||||||
|
local additionalOpts = {}
|
||||||
|
for k, v in pairs(opts) do
|
||||||
|
if k:find("ytdl_opt%d") and v ~= "" then
|
||||||
|
additionalOpts[k] = v
|
||||||
|
-- print("entry")
|
||||||
|
-- print(k .. v)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
local cachePath = opts.temp
|
||||||
|
|
||||||
|
local chapter_list = {}
|
||||||
|
local json = ""
|
||||||
|
local filesToDelete = {}
|
||||||
|
|
||||||
|
local function exists(file)
|
||||||
|
local ok, err, code = os.rename(file, file)
|
||||||
|
if not ok then
|
||||||
|
if code == 13 then -- Permission denied, but it exists
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return ok, err
|
||||||
|
end
|
||||||
|
local function useNewLoadfile()
|
||||||
|
for _, c in pairs(mp.get_property_native("command-list")) do
|
||||||
|
if c["name"] == "loadfile" then
|
||||||
|
for _, a in pairs(c["args"]) do
|
||||||
|
if a["name"] == "index" then
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
--from ytdl_hook
|
||||||
|
local function time_to_secs(time_string)
|
||||||
|
local ret
|
||||||
|
local a, b, c = time_string:match("(%d+):(%d%d?):(%d%d)")
|
||||||
|
if a ~= nil then
|
||||||
|
ret = (a * 3600 + b * 60 + c)
|
||||||
|
else
|
||||||
|
a, b = time_string:match("(%d%d?):(%d%d)")
|
||||||
|
if a ~= nil then
|
||||||
|
ret = (a * 60 + b)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return ret
|
||||||
|
end
|
||||||
|
local function extract_chapters(data, video_length)
|
||||||
|
local ret = {}
|
||||||
|
for line in data:gmatch("[^\r\n]+") do
|
||||||
|
local time = time_to_secs(line)
|
||||||
|
if time and (time < video_length) then
|
||||||
|
table.insert(ret, { time = time, title = line })
|
||||||
|
end
|
||||||
|
end
|
||||||
|
table.sort(ret, function(a, b) return a.time < b.time end)
|
||||||
|
return ret
|
||||||
|
end
|
||||||
|
local function chapters()
|
||||||
|
if json.chapters then
|
||||||
|
for i = 1, #json.chapters do
|
||||||
|
local chapter = json.chapters[i]
|
||||||
|
local title = chapter.title or ""
|
||||||
|
if title == "" then
|
||||||
|
title = string.format('Chapter %02d', i)
|
||||||
|
end
|
||||||
|
table.insert(chapter_list, { time = chapter.start_time, title = title })
|
||||||
|
end
|
||||||
|
elseif not (json.description == nil) and not (json.duration == nil) then
|
||||||
|
chapter_list = extract_chapters(json.description, json.duration)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
--end ytdl_hook
|
||||||
|
local title = ""
|
||||||
|
local fVideo = ""
|
||||||
|
local fAudio = ""
|
||||||
|
local function load_files(dtitle, destination, audio, wait)
|
||||||
|
if wait then
|
||||||
|
if exists(destination .. ".mka") then
|
||||||
|
print("---wait success: found mka---")
|
||||||
|
audio = "audio-file=" .. destination .. '.mka,'
|
||||||
|
else
|
||||||
|
print("---could not find mka after wait, audio may be missing---")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
-- if audio ~= "" then
|
||||||
|
-- table.insert(filesToDelete, destination .. ".mka")
|
||||||
|
-- end
|
||||||
|
-- table.insert(filesToDelete, destination .. ".mkv")
|
||||||
|
dtitle = dtitle:gsub("-" .. ("[%w_-]"):rep(11) .. "$", "")
|
||||||
|
dtitle = dtitle:gsub("^" .. ("%d"):rep(10) .. "%-", "")
|
||||||
|
if useNewLoadfile() then
|
||||||
|
mp.commandv("loadfile", destination .. ".mkv", "append", -1,
|
||||||
|
audio .. 'force-media-title="' .. dtitle .. '",demuxer-max-back-bytes=1MiB,demuxer-max-bytes=3MiB,ytdl=no')
|
||||||
|
else
|
||||||
|
mp.commandv("loadfile", destination .. ".mkv", "append",
|
||||||
|
audio .. 'force-media-title="' .. dtitle .. '",demuxer-max-back-bytes=1MiB,demuxer-max-bytes=3MiB,ytdl=no') --,sub-file="..destination..".en.vtt") --in case they are not set up to autoload
|
||||||
|
end
|
||||||
|
mp.commandv("playlist_move", mp.get_property("playlist-count") - 1, nextIndex)
|
||||||
|
mp.commandv("playlist_remove", nextIndex + 1)
|
||||||
|
caught = true
|
||||||
|
title = ""
|
||||||
|
-- pop = true
|
||||||
|
end
|
||||||
|
|
||||||
|
local listenID = ""
|
||||||
|
local function listener(event)
|
||||||
|
if not caught and event.prefix == mp.get_script_name() and string.find(event.text, listenID) then
|
||||||
|
local destination = string.match(event.text, "%[download%] Destination: (.+).mkv") or
|
||||||
|
string.match(event.text, "%[download%] (.+).mkv has already been downloaded")
|
||||||
|
-- if destination then print("---"..cachePath) end;
|
||||||
|
if destination and string.find(destination, string.gsub(cachePath, '~/', '')) then
|
||||||
|
-- print(listenID)
|
||||||
|
mp.unregister_event(listener)
|
||||||
|
_, title = utils.split_path(destination)
|
||||||
|
local audio = ""
|
||||||
|
if fAudio == "" then
|
||||||
|
load_files(title, destination, audio, false)
|
||||||
|
else
|
||||||
|
if exists(destination .. ".mka") then
|
||||||
|
audio = "audio-file=" .. destination .. '.mka,'
|
||||||
|
load_files(title, destination, audio, false)
|
||||||
|
else
|
||||||
|
print("---expected mka but could not find it, waiting for 2 seconds---")
|
||||||
|
mp.add_timeout(2, function()
|
||||||
|
load_files(title, destination, audio, true)
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
--from ytdl_hook
|
||||||
|
mp.add_hook("on_preloaded", 10, function()
|
||||||
|
if string.find(mp.get_property("path"), cachePath) then
|
||||||
|
chapters()
|
||||||
|
if next(chapter_list) ~= nil then
|
||||||
|
mp.set_property_native("chapter-list", chapter_list)
|
||||||
|
chapter_list = {}
|
||||||
|
json = ""
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
--end ytdl_hook
|
||||||
|
function dump(o)
|
||||||
|
if type(o) == 'table' then
|
||||||
|
local s = '{ '
|
||||||
|
for k, v in pairs(o) do
|
||||||
|
if type(k) ~= 'number' then k = '"' .. k .. '"' end
|
||||||
|
s = s .. '[' .. k .. '] = ' .. dump(v) .. ','
|
||||||
|
end
|
||||||
|
return s .. '} '
|
||||||
|
else
|
||||||
|
return tostring(o)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function addOPTS(old)
|
||||||
|
for k, v in pairs(additionalOpts) do
|
||||||
|
-- print(k)
|
||||||
|
if string.find(v, "%s") then
|
||||||
|
for l, w in string.gmatch(v, "([-%w]+) (.+)") do
|
||||||
|
table.insert(old, l)
|
||||||
|
table.insert(old, w)
|
||||||
|
end
|
||||||
|
else
|
||||||
|
table.insert(old, v)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
-- print(dump(old))
|
||||||
|
return old
|
||||||
|
end
|
||||||
|
|
||||||
|
local AudioDownloadHandle = {}
|
||||||
|
local VideoDownloadHandle = {}
|
||||||
|
local JsonDownloadHandle = {}
|
||||||
|
local function download_files(id, success, result, error)
|
||||||
|
if result.killed_by_us then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
local jfile = cachePath .. "/" .. id .. ".json"
|
||||||
|
|
||||||
|
local jfileIO = io.open(jfile, "w")
|
||||||
|
jfileIO:write(result.stdout)
|
||||||
|
jfileIO:close()
|
||||||
|
json = utils.parse_json(result.stdout)
|
||||||
|
-- print(dump(json))
|
||||||
|
if json.requested_downloads[1].requested_formats ~= nil then
|
||||||
|
local args = { ytdl, "--no-continue", "-q", "-f", fAudio, "--restrict-filenames", "--no-playlist", "--no-part",
|
||||||
|
"-o", cachePath .. "/" .. id .. "-%(title)s-%(id)s.mka", "--load-info-json", jfile }
|
||||||
|
args = addOPTS(args)
|
||||||
|
AudioDownloadHandle = mp.command_native_async({
|
||||||
|
name = "subprocess",
|
||||||
|
args = args,
|
||||||
|
playback_only = false
|
||||||
|
}, function()
|
||||||
|
end)
|
||||||
|
else
|
||||||
|
fAudio = ""
|
||||||
|
fVideo = fVideo:gsub("bestvideo", "best")
|
||||||
|
fVideo = fVideo:gsub("bv", "best")
|
||||||
|
end
|
||||||
|
|
||||||
|
local args = { ytdl, "--no-continue", "-f", fVideo .. '/best', "--restrict-filenames", "--no-playlist",
|
||||||
|
"--no-part", "-o", cachePath .. "/" .. id .. "-%(title)s-%(id)s.mkv", "--load-info-json", jfile }
|
||||||
|
args = addOPTS(args)
|
||||||
|
VideoDownloadHandle = mp.command_native_async({
|
||||||
|
name = "subprocess",
|
||||||
|
args = args,
|
||||||
|
playback_only = false
|
||||||
|
}, function()
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function DL()
|
||||||
|
local index = tonumber(mp.get_property("playlist-pos"))
|
||||||
|
if mp.get_property("playlist/" .. index .. "/filename"):find("/videos$") and mp.get_property("playlist/" .. index + 1 .. "/filename"):find("/shorts$") then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
if tonumber(mp.get_property("playlist-pos-1")) > 0 and mp.get_property("playlist-pos-1") ~= mp.get_property("playlist-count") then
|
||||||
|
nextIndex = index + 1
|
||||||
|
local nextFile = mp.get_property("playlist/" .. nextIndex .. "/filename")
|
||||||
|
if nextFile and caught and nextFile:find("://", 0, false) then
|
||||||
|
caught = false
|
||||||
|
mp.enable_messages("info")
|
||||||
|
mp.register_event("log-message", listener)
|
||||||
|
local ytFormat = mp.get_property("ytdl-format")
|
||||||
|
fVideo = string.match(ytFormat, '(.+)%+.+//?') or 'bestvideo'
|
||||||
|
fAudio = string.match(ytFormat, '.+%+(.+)//?') or 'bestaudio'
|
||||||
|
-- print("start"..nextFile)
|
||||||
|
listenID = tostring(os.time())
|
||||||
|
local args = { ytdl, "--dump-single-json", "--no-simulate", "--skip-download",
|
||||||
|
"--restrict-filenames",
|
||||||
|
"--no-playlist", "--sub-lang", "en", "--write-sub", "--no-part", "-o",
|
||||||
|
cachePath .. "/" .. listenID .. "-%(title)s-%(id)s.%(ext)s", nextFile }
|
||||||
|
args = addOPTS(args)
|
||||||
|
-- print(dump(args))
|
||||||
|
table.insert(filesToDelete, listenID)
|
||||||
|
JsonDownloadHandle = mp.command_native_async({
|
||||||
|
name = "subprocess",
|
||||||
|
args = args,
|
||||||
|
capture_stdout = true,
|
||||||
|
capture_stderr = true,
|
||||||
|
playback_only = false
|
||||||
|
}, function(...)
|
||||||
|
download_files(listenID, ...)
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function clearCache()
|
||||||
|
-- print(pop)
|
||||||
|
|
||||||
|
--if pop == true then
|
||||||
|
mp.abort_async_command(AudioDownloadHandle)
|
||||||
|
mp.abort_async_command(VideoDownloadHandle)
|
||||||
|
mp.abort_async_command(JsonDownloadHandle)
|
||||||
|
-- for k, v in pairs(filesToDelete) do
|
||||||
|
-- print("remove: " .. v)
|
||||||
|
-- os.remove(v)
|
||||||
|
-- end
|
||||||
|
local ftd = io.open(cachePath .. "/temp.files", "a")
|
||||||
|
for k, v in pairs(filesToDelete) do
|
||||||
|
ftd:write(v .. "\n")
|
||||||
|
if package.config:sub(1, 1) ~= '/' then
|
||||||
|
os.execute('del /Q /F "' .. cachePath .. "\\" .. v .. '*"')
|
||||||
|
else
|
||||||
|
os.execute('rm -f ' .. cachePath .. "/" .. v .. "*")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
ftd:close()
|
||||||
|
print('clear')
|
||||||
|
mp.command("quit")
|
||||||
|
--end
|
||||||
|
end
|
||||||
|
mp.add_hook("on_unload", 50, function()
|
||||||
|
-- mp.abort_async_command(AudioDownloadHandle)
|
||||||
|
-- mp.abort_async_command(VideoDownloadHandle)
|
||||||
|
mp.abort_async_command(JsonDownloadHandle)
|
||||||
|
mp.unregister_event(listener)
|
||||||
|
caught = true
|
||||||
|
listenID = "resetYtdlPreloadListener"
|
||||||
|
-- print(listenID)
|
||||||
|
end)
|
||||||
|
|
||||||
|
local skipInitial
|
||||||
|
mp.observe_property("playlist-count", "number", function()
|
||||||
|
if skipInitial then
|
||||||
|
DL()
|
||||||
|
else
|
||||||
|
skipInitial = true
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
|
||||||
|
--from ytdl_hook
|
||||||
|
local platform_is_windows = (package.config:sub(1, 1) == "\\")
|
||||||
|
local o = {
|
||||||
|
exclude = "",
|
||||||
|
try_ytdl_first = false,
|
||||||
|
use_manifests = false,
|
||||||
|
all_formats = false,
|
||||||
|
force_all_formats = true,
|
||||||
|
ytdl_path = "",
|
||||||
|
}
|
||||||
|
local paths_to_search = { "yt-dlp", "yt-dlp_x86", "youtube-dl" }
|
||||||
|
--local options = require 'mp.options'
|
||||||
|
options.read_options(o, "ytdl_hook")
|
||||||
|
|
||||||
|
local separator = platform_is_windows and ";" or ":"
|
||||||
|
if o.ytdl_path:match("[^" .. separator .. "]") then
|
||||||
|
paths_to_search = {}
|
||||||
|
for path in o.ytdl_path:gmatch("[^" .. separator .. "]+") do
|
||||||
|
table.insert(paths_to_search, path)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function exec(args)
|
||||||
|
local ret = mp.command_native({
|
||||||
|
name = "subprocess",
|
||||||
|
args = args,
|
||||||
|
capture_stdout = true,
|
||||||
|
capture_stderr = true
|
||||||
|
})
|
||||||
|
return ret.status, ret.stdout, ret, ret.killed_by_us
|
||||||
|
end
|
||||||
|
|
||||||
|
local msg = require 'mp.msg'
|
||||||
|
local command = {}
|
||||||
|
for _, path in pairs(paths_to_search) do
|
||||||
|
-- search for youtube-dl in mpv's config dir
|
||||||
|
local exesuf = platform_is_windows and ".exe" or ""
|
||||||
|
local ytdl_cmd = mp.find_config_file(path .. exesuf)
|
||||||
|
if ytdl_cmd then
|
||||||
|
msg.verbose("Found youtube-dl at: " .. ytdl_cmd)
|
||||||
|
ytdl = ytdl_cmd
|
||||||
|
break
|
||||||
|
else
|
||||||
|
msg.verbose("No youtube-dl found with path " .. path .. exesuf .. " in config directories")
|
||||||
|
--search in PATH
|
||||||
|
command[1] = path
|
||||||
|
es, json, result, aborted = exec(command)
|
||||||
|
if result.error_string == "init" then
|
||||||
|
msg.verbose("youtube-dl with path " .. path .. exesuf .. " not found in PATH or not enough permissions")
|
||||||
|
else
|
||||||
|
msg.verbose("Found youtube-dl with path " .. path .. exesuf .. " in PATH")
|
||||||
|
ytdl = path
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
--end ytdl_hook
|
||||||
|
|
||||||
|
mp.register_event("start-file", DL)
|
||||||
|
mp.register_event("shutdown", clearCache)
|
||||||
|
local ftd = io.open(cachePath .. "/temp.files", "r")
|
||||||
|
while ftd ~= nil do
|
||||||
|
local line = ftd:read()
|
||||||
|
if line == nil or line == "" then
|
||||||
|
ftd:close()
|
||||||
|
io.open(cachePath .. "/temp.files", "w"):close()
|
||||||
|
break
|
||||||
|
end
|
||||||
|
-- print("DEL::"..line)
|
||||||
|
if package.config:sub(1, 1) ~= '/' then
|
||||||
|
os.execute('del /Q /F "' .. cachePath .. "\\" .. line .. '*" >nul 2>nul')
|
||||||
|
else
|
||||||
|
os.execute('rm -f ' .. cachePath .. "/" .. line .. "* &> /dev/null")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
Reference in New Issue
Block a user