Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Doesn't work with ytmusicapi==1.x.x and pytube==15.x.x #71

Open
cprn opened this issue Jan 29, 2023 · 14 comments
Open

Doesn't work with ytmusicapi==1.x.x and pytube==15.x.x #71

cprn opened this issue Jan 29, 2023 · 14 comments

Comments

@cprn
Copy link

cprn commented Jan 29, 2023

Both ytmusicapi and pytube got updated beyond of what version 0.3.8 works with so the workaround is to install older versions instead:

pip uninstall ytmusicapi
pip install ytmusicapi==0.25.0
pip install pytube==12.1.2

(then restart mopidy)

@cprn cprn changed the title Doesn't work with ytmusicapi==0.25.0 Doesn't work with ytmusicapi==1.x.x and pytube==15.x.x May 19, 2023
@cprn
Copy link
Author

cprn commented May 19, 2023

Maybe this should be in README.md for the time being?

@herkow
Copy link

herkow commented Jun 25, 2023

Check this https://github.com/pytube/pytube/pull/1691

I've been searching the web for two days, but finally found the solution.

Rename your actual cipher.py to cipher.py.old o whatever. Create a new file named cipher.py, and paste the code from the last fixed cipher.py on git https://github.com/yogeshgirmal/pytube/blob/b0c9de965f7ec8b3140cc5a5a9eb15c039a5bd79/pytube/cipher.py

Restart mopidy, done.

@selurvedu
Copy link

selurvedu commented Aug 12, 2023

@herkow this needs one more patch now: pytube/pytube#1756

Upd: still doesn't work for me. Some other people report that the fix doesn't work for them too.

@herkow
Copy link

herkow commented Aug 12, 2023

@selurvedu Today I tried to play some music and it didn't work.
Official Pytube cipher.py has not been modified in about a year. I could spend weeks ranting about python... The solution that worked for me is in this repo https://github.com/yogeshgirmal/pytube/tree/b0c9de965f7ec8b3140cc5a5a9eb15c039a5bd79/pytube wich is not offcial. Today it was NOT working. But I've modified the line:

var_regex = re.compile(r"^\w+\W")

to

var_regex = re.compile(r"^\$*\w+\W")

and now it's working. Make sure you are using the cipher.py from the yogeshgirmal repo, not the official.

@selurvedu
Copy link

@herkow that's exactly what I did. Just tried it again and still getting pytube.exceptions.RegexMatchError: get_throttling_function_name: could not find match for multiple. I checked if I'm editing the right file – I do.

@herkow
Copy link

herkow commented Aug 12, 2023

@herkow that's exactly what I did. Just tried it again and still getting pytube.exceptions.RegexMatchError: get_throttling_function_name: could not find match for multiple. I checked if I'm editing the right file – I do.

Let's give it a last shot... rename your actual cipher.py to whatever you like, create a new one and paste this:

"""
This module contains all logic necessary to decipher the signature.

YouTube's strategy to restrict downloading videos is to send a ciphered version
of the signature to the client, along with the decryption algorithm obfuscated
in JavaScript. For the clients to play the videos, JavaScript must take the
ciphered version, cycle it through a series of "transform functions," and then
signs the media URL with the output.

This module is responsible for (1) finding and extracting those "transform
functions" (2) maps them to Python equivalents and (3) taking the ciphered
signature and decoding it.

"""
import logging
import re
from itertools import chain
from typing import Any, Callable, Dict, List, Optional, Tuple

from pytube.exceptions import ExtractError, RegexMatchError
from pytube.helpers import cache, regex_search
from pytube.parser import find_object_from_startpoint, throttling_array_split

logger = logging.getLogger(__name__)


class Cipher:
    def __init__(self, js: str):
        self.transform_plan: List[str] = get_transform_plan(js)
        var_regex = re.compile(r"^\$*\w+\W")
        var_match = var_regex.search(self.transform_plan[0])
        if not var_match:
            raise RegexMatchError(
                caller="__init__", pattern=var_regex.pattern
            )
        var = var_match.group(0)[:-1]
        self.transform_map = get_transform_map(js, var)
        self.js_func_patterns = [
            r"\w+\.(\w+)\(\w,(\d+)\)",
            r"\w+\[(\"\w+\")\]\(\w,(\d+)\)"
        ]

        self.throttling_plan = get_throttling_plan(js)
        self.throttling_array = get_throttling_function_array(js)

        self.calculated_n = None

    def calculate_n(self, initial_n: list):
        """Converts n to the correct value to prevent throttling."""
        if self.calculated_n:
            return self.calculated_n

        # First, update all instances of 'b' with the list(initial_n)
        for i in range(len(self.throttling_array)):
            if self.throttling_array[i] == 'b':
                self.throttling_array[i] = initial_n

        for step in self.throttling_plan:
            curr_func = self.throttling_array[int(step[0])]
            if not callable(curr_func):
                logger.debug(f'{curr_func} is not callable.')
                logger.debug(f'Throttling array:\n{self.throttling_array}\n')
                raise ExtractError(f'{curr_func} is not callable.')

            first_arg = self.throttling_array[int(step[1])]

            if len(step) == 2:
                curr_func(first_arg)
            elif len(step) == 3:
                second_arg = self.throttling_array[int(step[2])]
                curr_func(first_arg, second_arg)

        self.calculated_n = ''.join(initial_n)
        return self.calculated_n

    def get_signature(self, ciphered_signature: str) -> str:
        """Decipher the signature.

        Taking the ciphered signature, applies the transform functions.

        :param str ciphered_signature:
            The ciphered signature sent in the ``player_config``.
        :rtype: str
        :returns:
            Decrypted signature required to download the media content.
        """
        signature = list(ciphered_signature)

        for js_func in self.transform_plan:
            name, argument = self.parse_function(js_func)  # type: ignore
            signature = self.transform_map[name](signature, argument)
            logger.debug(
                "applied transform function\n"
                "output: %s\n"
                "js_function: %s\n"
                "argument: %d\n"
                "function: %s",
                "".join(signature),
                name,
                argument,
                self.transform_map[name],
            )

        return "".join(signature)

    @cache
    def parse_function(self, js_func: str) -> Tuple[str, int]:
        """Parse the Javascript transform function.

        Break a JavaScript transform function down into a two element ``tuple``
        containing the function name and some integer-based argument.

        :param str js_func:
            The JavaScript version of the transform function.
        :rtype: tuple
        :returns:
            two element tuple containing the function name and an argument.

        **Example**:

        parse_function('DE.AJ(a,15)')
        ('AJ', 15)

        """
        logger.debug("parsing transform function")
        for pattern in self.js_func_patterns:
            regex = re.compile(pattern)
            parse_match = regex.search(js_func)
            if parse_match:
                fn_name, fn_arg = parse_match.groups()
                return fn_name, int(fn_arg)

        raise RegexMatchError(
            caller="parse_function", pattern="js_func_patterns"
        )


def get_initial_function_name(js: str) -> str:
    """Extract the name of the function responsible for computing the signature.
    :param str js:
        The contents of the base.js asset file.
    :rtype: str
    :returns:
        Function name from regex match
    """

    function_patterns = [
        r"\b[cs]\s*&&\s*[adf]\.set\([^,]+\s*,\s*encodeURIComponent\s*\(\s*(?P<sig>[a-zA-Z0-9$]+)\(",  # noqa: E501
        r"\b[a-zA-Z0-9]+\s*&&\s*[a-zA-Z0-9]+\.set\([^,]+\s*,\s*encodeURIComponent\s*\(\s*(?P<sig>[a-zA-Z0-9$]+)\(",  # noqa: E501
        r'(?:\b|[^a-zA-Z0-9$])(?P<sig>[a-zA-Z0-9$]{2})\s*=\s*function\(\s*a\s*\)\s*{\s*a\s*=\s*a\.split\(\s*""\s*\)',  # noqa: E501
        r'(?P<sig>[a-zA-Z0-9$]+)\s*=\s*function\(\s*a\s*\)\s*{\s*a\s*=\s*a\.split\(\s*""\s*\)',  # noqa: E501
        r'(["\'])signature\1\s*,\s*(?P<sig>[a-zA-Z0-9$]+)\(',
        r"\.sig\|\|(?P<sig>[a-zA-Z0-9$]+)\(",
        r"yt\.akamaized\.net/\)\s*\|\|\s*.*?\s*[cs]\s*&&\s*[adf]\.set\([^,]+\s*,\s*(?:encodeURIComponent\s*\()?\s*(?P<sig>[a-zA-Z0-9$]+)\(",  # noqa: E501
        r"\b[cs]\s*&&\s*[adf]\.set\([^,]+\s*,\s*(?P<sig>[a-zA-Z0-9$]+)\(",  # noqa: E501
        r"\b[a-zA-Z0-9]+\s*&&\s*[a-zA-Z0-9]+\.set\([^,]+\s*,\s*(?P<sig>[a-zA-Z0-9$]+)\(",  # noqa: E501
        r"\bc\s*&&\s*a\.set\([^,]+\s*,\s*\([^)]*\)\s*\(\s*(?P<sig>[a-zA-Z0-9$]+)\(",  # noqa: E501
        r"\bc\s*&&\s*[a-zA-Z0-9]+\.set\([^,]+\s*,\s*\([^)]*\)\s*\(\s*(?P<sig>[a-zA-Z0-9$]+)\(",  # noqa: E501
        r"\bc\s*&&\s*[a-zA-Z0-9]+\.set\([^,]+\s*,\s*\([^)]*\)\s*\(\s*(?P<sig>[a-zA-Z0-9$]+)\(",  # noqa: E501
    ]
    logger.debug("finding initial function name")
    for pattern in function_patterns:
        regex = re.compile(pattern)
        function_match = regex.search(js)
        if function_match:
            logger.debug("finished regex search, matched: %s", pattern)
            return function_match.group(1)

    raise RegexMatchError(
        caller="get_initial_function_name", pattern="multiple"
    )


def get_transform_plan(js: str) -> List[str]:
    """Extract the "transform plan".

    The "transform plan" is the functions that the ciphered signature is
    cycled through to obtain the actual signature.

    :param str js:
        The contents of the base.js asset file.

    **Example**:

    ['DE.AJ(a,15)',
    'DE.VR(a,3)',
    'DE.AJ(a,51)',
    'DE.VR(a,3)',
    'DE.kT(a,51)',
    'DE.kT(a,8)',
    'DE.VR(a,3)',
    'DE.kT(a,21)']
    """
    name = re.escape(get_initial_function_name(js))
    pattern = r"%s=function\(\w\){[a-z=\.\(\"\)]*;(.*);(?:.+)}" % name
    logger.debug("getting transform plan")
    return regex_search(pattern, js, group=1).split(";")


def get_transform_object(js: str, var: str) -> List[str]:
    """Extract the "transform object".

    The "transform object" contains the function definitions referenced in the
    "transform plan". The ``var`` argument is the obfuscated variable name
    which contains these functions, for example, given the function call
    ``DE.AJ(a,15)`` returned by the transform plan, "DE" would be the var.

    :param str js:
        The contents of the base.js asset file.
    :param str var:
        The obfuscated variable name that stores an object with all functions
        that descrambles the signature.

    **Example**:

    >>> get_transform_object(js, 'DE')
    ['AJ:function(a){a.reverse()}',
    'VR:function(a,b){a.splice(0,b)}',
    'kT:function(a,b){var c=a[0];a[0]=a[b%a.length];a[b]=c}']

    """
    pattern = r"var %s={(.*?)};" % re.escape(var)
    logger.debug("getting transform object")
    regex = re.compile(pattern, flags=re.DOTALL)
    transform_match = regex.search(js)
    if not transform_match:
        raise RegexMatchError(caller="get_transform_object", pattern=pattern)

    return transform_match.group(1).replace("\n", " ").split(", ")


def get_transform_map(js: str, var: str) -> Dict:
    """Build a transform function lookup.

    Build a lookup table of obfuscated JavaScript function names to the
    Python equivalents.

    :param str js:
        The contents of the base.js asset file.
    :param str var:
        The obfuscated variable name that stores an object with all functions
        that descrambles the signature.

    """
    transform_object = get_transform_object(js, var)
    mapper = {}
    for obj in transform_object:
        # AJ:function(a){a.reverse()} => AJ, function(a){a.reverse()}
        name, function = obj.split(":", 1)
        fn = map_functions(function)
        mapper[name] = fn
    return mapper


def get_throttling_function_name(js: str) -> str:
    """Extract the name of the function that computes the throttling parameter.

    :param str js:
        The contents of the base.js asset file.
    :rtype: str
    :returns:
        The name of the function used to compute the throttling parameter.
    """
    function_patterns = [
        # https://github.com/ytdl-org/youtube-dl/issues/29326#issuecomment-865985377
        # https://github.com/yt-dlp/yt-dlp/commit/48416bc4a8f1d5ff07d5977659cb8ece7640dcd8
        # var Bpa = [iha];
        # ...
        # a.C && (b = a.get("n")) && (b = Bpa[0](b), a.set("n", b),
        # Bpa.length || iha("")) }};
        # In the above case, `iha` is the relevant function name
        r'a\.[a-zA-Z]\s*&&\s*\([a-z]\s*=\s*a\.get\("n"\)\)\s*&&\s*'
        r'\([a-z]\s*=\s*([a-zA-Z0-9$]+)(\[\d+\])?\([a-z]\)',
    ]
    logger.debug('Finding throttling function name')
    for pattern in function_patterns:
        regex = re.compile(pattern)
        function_match = regex.search(js)
        if function_match:
            logger.debug("finished regex search, matched: %s", pattern)
            if len(function_match.groups()) == 1:
                return function_match.group(1)
            idx = function_match.group(2)
            if idx:
                idx = idx.strip("[]")
                array = re.search(
                    r'var {nfunc}\s*=\s*(\[.+?\]);'.format(
                        nfunc=re.escape(function_match.group(1))),
                    js
                )
                if array:
                    array = array.group(1).strip("[]").split(",")
                    array = [x.strip() for x in array]
                    return array[int(idx)]

    raise RegexMatchError(
        caller="get_throttling_function_name", pattern="multiple"
    )


def get_throttling_function_code(js: str) -> str:
    """Extract the raw code for the throttling function.

    :param str js:
        The contents of the base.js asset file.
    :rtype: str
    :returns:
        The name of the function used to compute the throttling parameter.
    """
    # Begin by extracting the correct function name
    name = re.escape(get_throttling_function_name(js))

    # Identify where the function is defined
    pattern_start = r"%s=function\(\w\)" % name
    regex = re.compile(pattern_start)
    match = regex.search(js)

    # Extract the code within curly braces for the function itself, and merge any split lines
    code_lines_list = find_object_from_startpoint(js, match.span()[1]).split('\n')
    joined_lines = "".join(code_lines_list)

    # Prepend function definition (e.g. `Dea=function(a)`)
    return match.group(0) + joined_lines


def get_throttling_function_array(js: str) -> List[Any]:
    """Extract the "c" array.

    :param str js:
        The contents of the base.js asset file.
    :returns:
        The array of various integers, arrays, and functions.
    """
    raw_code = get_throttling_function_code(js)

    array_start = r",c=\["
    array_regex = re.compile(array_start)
    match = array_regex.search(raw_code)

    array_raw = find_object_from_startpoint(raw_code, match.span()[1] - 1)
    str_array = throttling_array_split(array_raw)

    converted_array = []
    for el in str_array:
        try:
            converted_array.append(int(el))
            continue
        except ValueError:
            # Not an integer value.
            pass

        if el == 'null':
            converted_array.append(None)
            continue

        if el.startswith('"') and el.endswith('"'):
            # Convert e.g. '"abcdef"' to string without quotation marks, 'abcdef'
            converted_array.append(el[1:-1])
            continue

        if el.startswith('function'):
            mapper = (
                (r"{for\(\w=\(\w%\w\.length\+\w\.length\)%\w\.length;\w--;\)\w\.unshift\(\w.pop\(\)\)}", throttling_unshift),  # noqa:E501
                (r"{\w\.reverse\(\)}", throttling_reverse),
                (r"{\w\.push\(\w\)}", throttling_push),
                (r";var\s\w=\w\[0\];\w\[0\]=\w\[\w\];\w\[\w\]=\w}", throttling_swap),
                (r"case\s\d+", throttling_cipher_function),
                (r"\w\.splice\(0,1,\w\.splice\(\w,1,\w\[0\]\)\[0\]\)", throttling_nested_splice),  # noqa:E501
                (r";\w\.splice\(\w,1\)}", js_splice),
                (r"\w\.splice\(-\w\)\.reverse\(\)\.forEach\(function\(\w\){\w\.unshift\(\w\)}\)", throttling_prepend),  # noqa:E501
                (r"for\(var \w=\w\.length;\w;\)\w\.push\(\w\.splice\(--\w,1\)\[0\]\)}", throttling_reverse),  # noqa:E501
            )

            found = False
            for pattern, fn in mapper:
                if re.search(pattern, el):
                    converted_array.append(fn)
                    found = True
            if found:
                continue

        converted_array.append(el)

    # Replace null elements with array itself
    for i in range(len(converted_array)):
        if converted_array[i] is None:
            converted_array[i] = converted_array

    return converted_array


def get_throttling_plan(js: str):
    """Extract the "throttling plan".

    The "throttling plan" is a list of tuples used for calling functions
    in the c array. The first element of the tuple is the index of the
    function to call, and any remaining elements of the tuple are arguments
    to pass to that function.

    :param str js:
        The contents of the base.js asset file.
    :returns:
        The full function code for computing the throttlign parameter.
    """
    raw_code = get_throttling_function_code(js)

    transform_start = r"try{"
    plan_regex = re.compile(transform_start)
    match = plan_regex.search(raw_code)

    transform_plan_raw = find_object_from_startpoint(raw_code, match.span()[1] - 1)

    # Steps are either c[x](c[y]) or c[x](c[y],c[z])
    step_start = r"c\[(\d+)\]\(c\[(\d+)\](,c(\[(\d+)\]))?\)"
    step_regex = re.compile(step_start)
    matches = step_regex.findall(transform_plan_raw)
    transform_steps = []
    for match in matches:
        if match[4] != '':
            transform_steps.append((match[0],match[1],match[4]))
        else:
            transform_steps.append((match[0],match[1]))

    return transform_steps


def reverse(arr: List, _: Optional[Any]):
    """Reverse elements in a list.

    This function is equivalent to:

    .. code-block:: javascript

        function(a, b) { a.reverse() }

    This method takes an unused ``b`` variable as their transform functions
    universally sent two arguments.

    **Example**:

    >>> reverse([1, 2, 3, 4])
    [4, 3, 2, 1]
    """
    return arr[::-1]


def splice(arr: List, b: int):
    """Add/remove items to/from a list.

    This function is equivalent to:

    .. code-block:: javascript

        function(a, b) { a.splice(0, b) }

    **Example**:

    >>> splice([1, 2, 3, 4], 2)
    [1, 2]
    """
    return arr[b:]


def swap(arr: List, b: int):
    """Swap positions at b modulus the list length.

    This function is equivalent to:

    .. code-block:: javascript

        function(a, b) { var c=a[0];a[0]=a[b%a.length];a[b]=c }

    **Example**:

    >>> swap([1, 2, 3, 4], 2)
    [3, 2, 1, 4]
    """
    r = b % len(arr)
    return list(chain([arr[r]], arr[1:r], [arr[0]], arr[r + 1 :]))


def throttling_reverse(arr: list):
    """Reverses the input list.

    Needs to do an in-place reversal so that the passed list gets changed.
    To accomplish this, we create a reversed copy, and then change each
    indvidual element.
    """
    reverse_copy = arr.copy()[::-1]
    for i in range(len(reverse_copy)):
        arr[i] = reverse_copy[i]


def throttling_push(d: list, e: Any):
    """Pushes an element onto a list."""
    d.append(e)


def throttling_mod_func(d: list, e: int):
    """Perform the modular function from the throttling array functions.

    In the javascript, the modular operation is as follows:
    e = (e % d.length + d.length) % d.length

    We simply translate this to python here.
    """
    return (e % len(d) + len(d)) % len(d)


def throttling_unshift(d: list, e: int):
    """Rotates the elements of the list to the right.

    In the javascript, the operation is as follows:
    for(e=(e%d.length+d.length)%d.length;e--;)d.unshift(d.pop())
    """
    e = throttling_mod_func(d, e)
    new_arr = d[-e:] + d[:-e]
    d.clear()
    for el in new_arr:
        d.append(el)


def throttling_cipher_function(d: list, e: str):
    """This ciphers d with e to generate a new list.

    In the javascript, the operation is as follows:
    var h = [A-Za-z0-9-_], f = 96;  // simplified from switch-case loop
    d.forEach(
        function(l,m,n){
            this.push(
                n[m]=h[
                    (h.indexOf(l)-h.indexOf(this[m])+m-32+f--)%h.length
                ]
            )
        },
        e.split("")
    )
    """
    h = list('ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_')
    f = 96
    # by naming it "this" we can more closely reflect the js
    this = list(e)

    # This is so we don't run into weirdness with enumerate while
    #  we change the input list
    copied_list = d.copy()

    for m, l in enumerate(copied_list):
        bracket_val = (h.index(l) - h.index(this[m]) + m - 32 + f) % len(h)
        this.append(
            h[bracket_val]
        )
        d[m] = h[bracket_val]
        f -= 1


def throttling_nested_splice(d: list, e: int):
    """Nested splice function in throttling js.

    In the javascript, the operation is as follows:
    function(d,e){
        e=(e%d.length+d.length)%d.length;
        d.splice(
            0,
            1,
            d.splice(
                e,
                1,
                d[0]
            )[0]
        )
    }

    While testing, all this seemed to do is swap element 0 and e,
    but the actual process is preserved in case there was an edge
    case that was not considered.
    """
    e = throttling_mod_func(d, e)
    inner_splice = js_splice(
        d,
        e,
        1,
        d[0]
    )
    js_splice(
        d,
        0,
        1,
        inner_splice[0]
    )


def throttling_prepend(d: list, e: int):
    """

    In the javascript, the operation is as follows:
    function(d,e){
        e=(e%d.length+d.length)%d.length;
        d.splice(-e).reverse().forEach(
            function(f){
                d.unshift(f)
            }
        )
    }

    Effectively, this moves the last e elements of d to the beginning.
    """
    start_len = len(d)
    # First, calculate e
    e = throttling_mod_func(d, e)

    # Then do the prepending
    new_arr = d[-e:] + d[:-e]

    # And update the input list
    d.clear()
    for el in new_arr:
        d.append(el)

    end_len = len(d)
    assert start_len == end_len


def throttling_swap(d: list, e: int):
    """Swap positions of the 0'th and e'th elements in-place."""
    e = throttling_mod_func(d, e)
    f = d[0]
    d[0] = d[e]
    d[e] = f


def js_splice(arr: list, start: int, delete_count=None, *items):
    """Implementation of javascript's splice function.

    :param list arr:
        Array to splice
    :param int start:
        Index at which to start changing the array
    :param int delete_count:
        Number of elements to delete from the array
    :param *items:
        Items to add to the array

    Reference: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/splice  # noqa:E501
    """
    # Special conditions for start value
    try:
        if start > len(arr):
            start = len(arr)
        # If start is negative, count backwards from end
        if start < 0:
            start = len(arr) - start
    except TypeError:
        # Non-integer start values are treated as 0 in js
        start = 0

    # Special condition when delete_count is greater than remaining elements
    if not delete_count or delete_count >= len(arr) - start:
        delete_count = len(arr) - start  # noqa: N806

    deleted_elements = arr[start:start + delete_count]

    # Splice appropriately.
    new_arr = arr[:start] + list(items) + arr[start + delete_count:]

    # Replace contents of input array
    arr.clear()
    for el in new_arr:
        arr.append(el)

    return deleted_elements


def map_functions(js_func: str) -> Callable:
    """For a given JavaScript transform function, return the Python equivalent.

    :param str js_func:
        The JavaScript version of the transform function.
    """
    mapper = (
        # function(a){a.reverse()}
        (r"{\w\.reverse\(\)}", reverse),
        # function(a,b){a.splice(0,b)}
        (r"{\w\.splice\(0,\w\)}", splice),
        # function(a,b){var c=a[0];a[0]=a[b%a.length];a[b]=c}
        (r"{var\s\w=\w\[0\];\w\[0\]=\w\[\w\%\w.length\];\w\[\w\]=\w}", swap),
        # function(a,b){var c=a[0];a[0]=a[b%a.length];a[b%a.length]=c}
        (
            r"{var\s\w=\w\[0\];\w\[0\]=\w\[\w\%\w.length\];\w\[\w\%\w.length\]=\w}",
            swap,
        ),
    )

    for pattern, fn in mapper:
        if re.search(pattern, js_func):
            return fn
    raise RegexMatchError(caller="map_functions", pattern="multiple")

Restart mopidy, and let's see if it works.

@cprn
Copy link
Author

cprn commented Nov 24, 2023

I just want to say I tested it and it still works without issues on up to date Ubuntu with the workaround I mentioned in the first post. Although I have same issues as everyone on Arch / Manjaro so it'd be beneficial if this PR got accepted.

@herkow
Copy link

herkow commented Dec 2, 2023

Something strange is happening now. When I "sing-in" to my account via the auth_json file, I can not play (and can not search) any tracks. But when Youtube de-authorizes me, I can search and play.

Is this happening to you to?

@cprn
Copy link
Author

cprn commented Jan 10, 2024

@herkow Sorry for late response. I switched to jmcdo29's fork of this repository. As soon as @jmcdo29 accepts my PR his fork will be working out of the box - it's already used by Arch user repo package.

edit: the PR got accepted, jmcdo29's repo is the most current one right now

dotlambda added a commit to dotlambda/nixpkgs that referenced this issue Jan 29, 2024
Just like the AUR and as suggested in
OzymandiasTheGreat/mopidy-ytmusic#71 we switch
to a fork which uses a more recent version of ytmusicapi.

Diff: jmcdo29/mopidy-ytmusic@v0.3.8...v0.3.9

Changelog: https://github.com/jmcdo29/mopidy-ytmusic/releases/tag/v0.3.9
@herkow
Copy link

herkow commented Mar 16, 2024

@herkow Sorry for late response. I switched to jmcdo29's fork of this repository. As soon as @jmcdo29 accepts my PR his fork will be working out of the box - it's already used by Arch user repo package.

edit: the PR got accepted, jmcdo29's repo is the most current one right now

I've installed from jmcdo29, but it cannot even setup:

hk@MEDIA:/etc/mopidy$ sudo mopidyctl ytmusic setup
Running "/usr/bin/mopidy --config /usr/share/mopidy/conf.d:/etc/mopidy/mopidy.conf ytmusic setup" as user mopidy
INFO     [MainThread] mopidy.__main__ Starting Mopidy 3.4.1
INFO     [MainThread] mopidy.config Loading config from builtin defaults
INFO     [MainThread] mopidy.config Loading config from file:///usr/share/mopidy/conf.d/mopidy.conf
INFO     [MainThread] mopidy.config Loading config from file:///etc/mopidy/mopidy.conf
INFO     [MainThread] mopidy.config Loading config from command line options
INFO     [MainThread] mopidy.__main__ Enabled extensions: file, softwaremixer, m3u, stream, http
INFO     [MainThread] mopidy.__main__ Disabled extensions: ytmusic
WARNING  [MainThread] mopidy.__main__ Found ytmusic configuration errors. The extension has been automatically disabled:
WARNING  [MainThread] mopidy.__main__   ytmusic/oauth_json config key not found.
WARNING  [MainThread] mopidy.__main__ Please fix the extension configuration errors or disable the extensions to silence these messages.
ERROR    [MainThread] mopidy.__main__ Unable to run command provided by disabled extension ytmusic

The main purpose of setup is to CREATE the key.

@cprn
Copy link
Author

cprn commented Mar 16, 2024

ytmusic/oauth_json config key not found.

You need oauth_json in your mopidy [ytmusic] section with a path to where you want to keep the oauth.json file as there's no default location for it, e.g.:

[ytmusic]
oauth_json = /home/herkow/.config/mopidy/oauth.json

Run setup after you add it, it should work - it worked for me when I made that change.

@herkow
Copy link

herkow commented Mar 16, 2024

ytmusic/oauth_json config key not found.

You need oauth_json in your mopidy [ytmusic] section with a path to where you want to keep the oauth.json file as there's no default location for it, e.g.:

[ytmusic]
oauth_json = /home/herkow/.config/mopidy/oauth.json

Run setup after you add it, it should work - it worked for me when I made that change.

Still unable to setup:

hk@MEDIA:/etc/mopidy$ sudo mopidyctl ytmusic setup
Running "/usr/bin/mopidy --config /usr/share/mopidy/conf.d:/etc/mopidy/mopidy.conf ytmusic setup" as user mopidy
INFO     [MainThread] mopidy.__main__ Starting Mopidy 3.4.1
INFO     [MainThread] mopidy.config Loading config from builtin defaults
INFO     [MainThread] mopidy.config Loading config from file:///usr/share/mopidy/conf.d/mopidy.conf
INFO     [MainThread] mopidy.config Loading config from file:///etc/mopidy/mopidy.conf
INFO     [MainThread] mopidy.config Loading config from command line options
INFO     [MainThread] mopidy.__main__ Enabled extensions: ytmusic, file, stream, m3u, softwaremixer, http
INFO     [MainThread] mopidy.__main__ Disabled extensions: none
ERROR    [MainThread] mopidy.__main__ cannot import name 'setup_oauth' from 'ytmusicapi.setup' (/usr/local/lib/python3.11/dist-packages/ytmusicapi/setup.py)
Traceback (most recent call last):
  File "/usr/lib/python3/dist-packages/mopidy/__main__.py", line 143, in main
    return args.command.run(args, proxied_config)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.11/dist-packages/mopidy_ytmusic/command.py", line 20, in run
    from ytmusicapi.setup import setup_oauth
ImportError: cannot import name 'setup_oauth' from 'ytmusicapi.setup' (/usr/local/lib/python3.11/dist-packages/ytmusicapi/setup.py)
Traceback (most recent call last):
  File "/usr/bin/mopidy", line 33, in <module>
    sys.exit(load_entry_point('Mopidy==3.4.1', 'console_scripts', 'mopidy')())
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/lib/python3/dist-packages/mopidy/__main__.py", line 143, in main
    return args.command.run(args, proxied_config)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.11/dist-packages/mopidy_ytmusic/command.py", line 20, in run
    from ytmusicapi.setup import setup_oauth
ImportError: cannot import name 'setup_oauth' from 'ytmusicapi.setup' (/usr/local/lib/python3.11/dist-packages/ytmusicapi/setup.py)
hk@MEDIA:/etc/mopidy$ 


@herkow
Copy link

herkow commented Mar 17, 2024

Well, I've been trying to solve this, and cloned the repo (which says v0.3.9) just in case git+ is doing something crazy... But when I check the installed version, still shows 0.3.8... Last shot was to copy the old auth.json to oauth.json and now YTMusic appears in the client but is not possible to search, I can only access to YTMUSIC lists (mosts palyed, best of my country, etc.) There is no way to excute ytmusic setup and of course is not possible to reauth.

Any ideas?

BTW, I'm using Debian 12.

More workaround here, and now I've switched to ytmusicapi 0.25.0. I can search and play, but still can't setup, and the reauth is missing something, since it never asks for the credentials:

hk@MEDIA:/etc/mopidy$ sudo mopidyctl ytmusic reauth
Running "/usr/bin/mopidy --config /usr/share/mopidy/conf.d:/etc/mopidy/mopidy.conf ytmusic reauth" as user mopidy
INFO     [MainThread] mopidy.__main__ Starting Mopidy 3.4.1
INFO     [MainThread] mopidy.config Loading config from builtin defaults
INFO     [MainThread] mopidy.config Loading config from file:///usr/share/mopidy/conf.d/mopidy.conf
INFO     [MainThread] mopidy.config Loading config from file:///etc/mopidy/mopidy.conf
INFO     [MainThread] mopidy.config Loading config from command line options
INFO     [MainThread] mopidy.__main__ Enabled extensions: m3u, softwaremixer, ytmusic, stream, http, file
INFO     [MainThread] mopidy.__main__ Disabled extensions: none
Updating credentials in  "/etc/mopidy/oauth.json"
Updating via oauth, follow the instructions from ytmusicapi
<ytmusicapi.ytmusic.YTMusic object at 0x7fed17393050>
Authentication JSON data saved to /etc/mopidy/oauth.json
hk@MEDIA:/etc/mopidy$ 

@cprn
Copy link
Author

cprn commented May 27, 2024

@herkow I have no idea what you've done but you definitely had some mismatch in your installed packages because this:

ImportError: cannot import name 'setup_oauth' from 'ytmusicapi.setup' (/usr/local/lib/python3.11/dist-packages/ytmusicapi/setup.py)

says you don't have setup_oauth function in the linked file and it should be there in the current version.

I see you've managed to fix that part and the function imports and calls but on my setup it opens the default browser (or at least displays the URL to open in the browser manually) to login to YT, however, I'm not using sudo or mopidyctl or global configs in /etc so maybe it's that. AFAICT it's working fine on my Manjaro/Arch setup out of the box. Since you don't have any further errors displayed I'd suggest to install all dependencies from scratch, make sure they're the newest versions, and try to debug the code. It should work OK as long as this line is called:

    return RefreshingToken.prompt_for_token(oauth_credentials, open_browser, filepath)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants