تحت غطاء عميل الروبوت Yandex.Music

المقدمة


مرحبا يا هابر! مرة أخرى ، أنا مع المقالة الثانية التي تؤثر على Yandex.Music API. الحالة مخططة ومذكورة في المقال الأول .

وصلت اليدين ، تم. اليوم سأتحدث عن لحظات مثيرة للاهتمام ، في رأيي ، موجودة في قاعدة التعليمات البرمجية الخاصة ببرنامج Telegram الخاص بي ، والتي تضع نفسها كعميل كامل للموسيقى. سنلمس أيضًا واجهة برمجة تطبيقات Yandex للتعرف على الموسيقى.

قبل الانتقال إلى قصة تنفيذ شيء معين نقطة تلو الأخرى ، سيكون من المفيد أن يكون لديك فكرة عن الروبوت نفسه وقدراته الوظيفية.

عرض فيديو العميل


في الجزء الرئيسي سأتحدث عن ما يلي:

  1. قم بتسجيل الدخول إلى حسابك من خلال الموقع على صفحات GitHub (لماذا ولماذا).
  2. تنسيق البيانات وتعبئتها واستخدامها في بيانات الأزرار.
  3. تحديث التوجيه وإصدار البيانات ورمي السياق إلى المعالجات.
  4. خدمات:
    • خدمة إعادة تحميل المسار في Telegram.
    • خدمة "الاشتراكات" لتلقي مسار مع إرسال حالة التنزيل.
  5. أبسط وأنيق تنفيذ التخزين المؤقت للاستعلام.
  6. التعرف على المسار عن طريق رسالة صوتية وكيف ظهر بشكل عام في الروبوت.
  7. ملاحظات صغيرة.

إذا كنت مهتمًا بنقطة واحدة على الأقل - مرحبًا بك في القط.
يعمل البوت مع المستخدمين الذين قاموا بتسجيل الدخول إلى حساباتهم ، ولكن لا. كل شيء يدور حول أربع فئات رئيسية للخدمة: الألبوم والفنان وقائمة التشغيل والمسار. جميع بيانات الكيان مدعومة ، هناك ترقيم الصفحات مع اختيار الصفحة. للتفاعل معهم ، يمكن للمستخدم المصرح له استخدام قائمتهم الشخصية ، والتي تتضمن: قوائم التشغيل الذكية وقائمة التشغيل "أحب" وقوائم التشغيل الخاصة بهم. بالنسبة للمستخدمين غير المخولين ، يتوفر البحث - إرسال استعلام بحث كرسالة عادية. الآن هناك فقط قائمة جافة بما هو موجود أيضًا: استقبال المسارات والكائنات الأخرى عبر رابط مباشر ، وتلقي كلمات الأغاني ، والقدرة على الإعجاب / عدم الإعجاب بالمسار والفنان ، وعرض المسارات المماثلة ، والتعرف على المسار عن طريق الرسائل الصوتية والمزيد.

الجزء الرئيسي


1. تسجيل الدخول إلى الحساب


في البداية ، لم يكن من المقرر أن يعمل البوت بدون ترخيص ، لذلك ، حتى أثناء التصميم ، تقرر كيف يجب أن يحدث الترخيص - من خلال موقعه على الإنترنت . كانت الحجج الرئيسية الأمن والشفافية. لا تريد بأي حال من الأحوال قبول تسجيلات دخول المستخدم وكلمات المرور في رسائل واضحة. والتفويض من كمبيوتر المستخدم هو أمر أساسي. لذلك ، تمت كتابة موقع للترخيص على React . إنه شكل من أشكال التفويض مع إعادة التوجيه إلى برنامج التتبُّع. تم أخذ قطعة بتفويض ومعالجة اختبار CAPTCHA من مكتبة Python وإعادة كتابتها في JavaScript .

للترخيص ، يتم استخدام رمز OAuth المميز ، والذي يتم نقله مرة أخرى إلى برنامج التتبُّع عن طريق الربط العميق.

يبدو الرابط النهائي لإعادة التوجيه كما يلي: t.me/music_yandex_bot؟start={oauth_token}

أردت الأفضل ، لكن المستخدمين لم يثقوا. تم التصريح لكل مستخدم العاشر فقط (في الوقت الذي كان فيه إلزاميًا). لذلك ، مع التحديثات التالية ، كان علي أن أشرح لماذا يجب أن تثق. تم نشر جميع العناصر حول HTTPS ، حصريًا حول الطلبات من جانب العميل ورمز المصدر المفتوح على الموقع للحصول على تفويض وتكرارها في رسالة ترحيب في الروبوت. تحسن.

أيضًا ، من بين عوامل انخفاض الإذن ، تبين أن عدم إمكانية الوصول إلى المرايا لـ t.me في روسيا ودعم المستخدمين الذين لديهم اشتراك فقط (المزيد عن هذا في الملاحظات ، الفقرة 7).

تم حل العنصر الأول باستخدام URI ( tg: // ) ، وفي الجزء السفلي ، يوجد بالفعل رابط إلى المرآة (إذا لم تعمل إعادة التوجيه التلقائية ولم يساعد الزر) ، والعنصر الثاني مرة أخرى في البند 7.


2. تنسيق البيانات


لهذا الروبوت ، قررت للمرة الأولى استخدام NoSQL DB - MongoDB . مفهوم: عند تضمين مستند ، عندما لا. أحب العمل مع mongoengine. لكنني لم أرغب حقًا في تخزين جميع بيانات الأزرار في المنزل ، ولدى العميل معرف السجل فقط في قاعدة البيانات. لقد شعرت بالرعب من كمية البيانات ، لكنني أردت أخذ خادم مونغو مجاني بحد 512 ميجابايت لتخزين البيانات. إن التوصل إلى إعادة استخدام جميلة للسجلات إذا كانت البيانات تتطابق مع العديد من الأزرار وتنظيف القديمة منها أصعب بكثير من تخزين كل شيء في الأزرار نفسها. بعد تحليل حجم البيانات المراد تخزينها ، استنتجت أنها مناسبة بسهولة.

في البداية ، استخدمت JSON للتو، لكنه رفضها بسرعة عندما واجه حدودًا. في Telegram ، لا يمكن أن تزيد محتويات بيانات الزر إلا عن 64 بايت في UTF-8 .

لذلك ، مع مطالبة أحد الأصدقاء ، بدأت في النظر في حزمة من وحدة الهيكل . لذلك ولدت أنواع الاستفسارات ، الشكل البدائي ، والتغليف والتفريغ. الآن يتم استخدامه في الروبوت في كل مكان.

التنسيق بسيط للغاية. البايت الأول هو النوع ، والثاني هو الإصدار. كل شيء آخر هو بيانات لنوع معين. يتم تخزين الأنواع كـ Enum ، لها معرف ، وهو البايت الأول. بالإضافة إلى المعرّف ، يحتوي كل نوع على تنسيق لحزم البيانات وتفريغها. على سبيل المثال: اكتب SHOW_TRACK_MENUأي تنسيق له القيمة "s؟" ، حيث يكون "s" هو المعرف الفريد للمسار و "؟" - هل المسار يحتوي على نص.

في مسارات تستخدم نوع السلسلة للأسباب التالية: أولا، ID المسار يمكن أن يكون سلسلة من ID والألبوم ID المسار من خلال القولون، وثانيا، قد يكون من UUID . المسارات باستخدام UUID - مسارات ذاتية التحميل ، متاحة فقط للمستخدم الذي قام بتنزيلها.

نظرًا لأن البيانات لا تتوافق دائمًا مع التنسيق ، على سبيل المثال ، يمكن تمثيل معرف المسار نفسه ببساطة عن طريق رقم ، قبل حزمه يجب أن يلقي في نوع التنسيق. في هذه الحالة ، ق. لذلك ، يوجد في الفصل طريقة تطبيع البيانات المنقولة للتغليف ، حتى لا تفعل ذلك بنفسك عند المرور إلى المنشئ.

الأوتار مكتفية ذاتيا وقادرة على تحديد طولها عند التغليف وأخذ هذا الطول في الاعتبار عند التفريغ.

لم يتم التخطيط لدعم الإصدارات الأقدم ، لذلك تم طرح استثناء إذا لم تتطابق الإصدارات. عند معالجة التحديثات ، والتي سأناقشها في الفقرة التالية ، يتم استدعاء المنطق الضروري.

نظرًا لأن Telegram يأكل حصريًا UTF-8 ، يتم ترميز البيانات المعبأة في base85 . نعم ، أنا أفقد السرعة هنا وأوفر الحجم الأصغر بدون استخدام base64 ، ولكن بالنظر إلى البيانات الصغيرة ، فإنني أعتبر استخدام base85 مناسبًا.

مصدر. ملف callback_data.py
import struct
import base64

from ext.query_types import QueryType


class BadDataVersion(Exception):
    pass


class CallbackData:
    ACTUAL_VERSION = 7
    BASE_FORMAT = '<BB'

    def __init__(self, type_: QueryType, data=None, version=None):
        self.type = type_
        self.version = version or CallbackData.ACTUAL_VERSION
        self.data = data

        if self.data is not None and not isinstance(self.data, list):
            self.data = [self.data]

        if self.data is not None:
            self.data = self._normalize_data_to_format(self.data)

    def __repr__(self):
        return f'<CallbackData> Type: {self.type} Version: {self.version} Data: {self.data}'

    def _normalize_data_to_format(self, data, bytes_object=False):
        normalized_data = data.copy()
        for i, c in enumerate(self.type.format):
            cast = str
            if c.lower() in 'bhilqn':
                cast = int
            elif c in 'efd':
                cast = float
            elif c == '?':
                cast = bool

            casted = cast(data[i])
            if bytes_object and cast == str:
                casted = casted.encode('utf-8')
            normalized_data[i] = casted

        return normalized_data

    @staticmethod
    def decode_type(callback_data):
        decoded = base64.b85decode(callback_data.encode('utf-8'))
        type_, version = struct.unpack(CallbackData.BASE_FORMAT, decoded[:2])

        if CallbackData.ACTUAL_VERSION != version:
            raise BadDataVersion()

        return QueryType(type_), version, decoded

    @classmethod
    def decode(cls, type_, version, decoded):
        start, data = 2, []

        if start < len(decoded):
            format_iter = iter(type_.format)

            while True:
                if start >= len(decoded):
                    break

                format_ = next(format_iter, type_.format[-1])

                decode_str = format_ in 'ps'
                if decode_str:
                    # struct.calcsize('b') = 1
                    length = list(struct.unpack('b', decoded[start: start + 1]))[0]
                    start += 1

                    format_ = f'{length}{format_}'

                step = struct.calcsize(format_)

                unpacked = list(struct.unpack(f'{format_}', decoded[start: start + step]))
                if decode_str:
                    unpacked[0] = unpacked[0].decode('UTF-8')

                data += unpacked
                start += step

        return cls(type_, data if data else None, version)

    def encode(self):
        encode = struct.pack(self.BASE_FORMAT, self.type.value, self.version)

        if self.data is not None:
            format_iter = iter(self.type.format)
            normalized_data = self._normalize_data_to_format(self.data, bytes_object=True)

            for data in normalized_data:
                format_ = next(format_iter, self.type.format[-1])

                if format_ in 'ps':
                    #        .  'b'  
                    # -      ,    > 36 
                    encode += struct.pack('b', len(data))
                    encode += struct.pack(f'{len(data)}{format_}', data)
                else:
                    encode += struct.pack(f'{format_}', data)

        return base64.b85encode(encode).decode('utf-8')



3. تحديثات التوجيه والسياق


يستخدم المشروع مكتبة python-telegram-bot للعمل مع Telegram Bot API . لديها بالفعل نظام لتسجيل معالجات لأنواع معينة من التحديثات التي وصلت ، وفلاتر للتعبيرات العادية ، والأوامر ، وما إلى ذلك. ولكن نظرا لبلدي تنسيق البيانات الخاصة وأنواع بلدي، وكان لي أن ترث من TelegramHandler وتنفيذ بلدي معالج .

يتم تمرير التحديث والسياق إلى كل معالج من خلال الوسائط. في هذه الحالة ، لدي سياق خاص بي وهو في Handler'eيتم تشكيله ، وهذا هو: استلام و / أو إضافة مستخدم إلى قاعدة البيانات ، والتحقق من ملاءمة الرمز المميز للوصول إلى الموسيقى ، وتهيئة عميل Yandex.Music ، اعتمادًا على حالة التفويض ومدى توفر الاشتراك.

بعيدًا عن Handler'a ، هناك معالجات أكثر تحديدًا ، على سبيل المثال ، CallbackQueryHandler . باستخدامه ، يتم تسجيل معالج لنوع معين من التحديث (النوع الخاص بي ، بتنسيق بيانات). للتحقق مما إذا كان هذا التحديث مناسبًا للمعالج الحالي ، لا يتم فك كل البيانات ، ولكن فقط أول وحدتي بايت. في هذه المرحلة ، يتم التحقق من الحاجة إلى تشغيل رد اتصال. فقط إذا كان تشغيل رد الاتصال ضروريًا - هل يتم تفريغ البيانات بالكامل ونقلها ككرغإلى المعالج النهائي. على الفور يتم إرسال البيانات التحليلية إلى ChatBase .

يتم التسجيل بالتسلسل ، والأولوية أعلى لمن سيتم تسجيله في وقت سابق (في الواقع ، كما هو الحال في توجيه Django ، وفي مشاريع أخرى). لذلك ، يعد تسجيل معالج إصدار قديم هو الأول بين معالجات CallBackQuery .

منطق معالجة الإصدار القديم بسيط - أبلغ المستخدم بهذا الأمر وأرسل البيانات المحدثة ، إن أمكن.

4. الخدمات


تتم تهيئة جميع الخدمات عندما يتم إطلاق البوت في فئة تحكم واحدة ، والتي يتم استخدامها عالميًا في أي مكان في البوت ( DJ ).

كل خدمة لها ThreadPoolExecutor مع عدد معين من العمال التي يتم إرسال المهام إليها.

إعادة تحميل مسار في Telegram


في الوقت الحالي ، لم تتم إعادة كتابة هذه الخدمة إلى User Bot لتجاوز الحد الأقصى لحجم الملف الذي تم تنزيله في Telegram. كما اتضح ، في Yandex.Music هناك ملفات أكبر من 50 ميغابايت - ملفات بودكاست.

تتحقق الخدمة من حجم الملف ، وفي حالة الزيادة ، توجه تنبيهًا للمستخدم. بفضل نظام التخزين المؤقت الموضح في الفقرة 5 ، يتحقق هذا من توفر واستلام كلمات الأغاني. يتم أيضًا تخزين المسارات مؤقتًا. يتم تخزين تجزئة الملف في قاعدة البيانات. إذا كان هناك واحد ، يتم إرسال صوت مع ذاكرة تخزين مؤقت معروفة.

في حالة عدم وجود ملف في قاعدة البيانات ، يتم تلقي ارتباط مباشر من Yandex.Music. على الرغم من أنه في الوقت الحالي ، لا يمتلك المستخدمون القدرة على تغيير إعدادات الجودة ، ولكن يتم تعيينهم جميعًا على القيم القياسية. يتم البحث عن الملف عن طريق معدل البت والترميز من إعدادات المستخدم.

يتم تنزيل الملف وغلافه كملف مؤقت. ملف مؤقت () ، وبعد ذلك يتم تحميلهما إلى Telegram. تجدر الإشارة إلى أن TG لا تتعرف دائمًا على مدة المسار بشكل صحيح ، لكنني عمومًا صامت بشأن الفنان والعنوان. لذلك ، يتم أخذ هذه البيانات من Yandex ، لحسن الحظ ، هناك فرصة لإرسالها إلى سلة التسوق.

عندما يتم إرسال ملف صوتي بواسطة هذه الخدمة ، يتم استدعاء finish_callback () ، مما يشير إلى الخدمة عن طريق الاشتراك حول نهاية التنزيل.

خدمة الاشتراك لتلقي المسارات وإرسال حالة التنزيل


لا يتم تحميل المسارات على الفور. من الممكن أن يكون العديد من المستخدمين قد طلبوا نفس المسار. في هذه الحالة ، يكون المستخدم الذي طلب المسار أولاً هو البادئ والمالك للمسار. عند إعادة التحميل لأكثر من ثانية ، تبدأ حالة التنزيل: "يرسل المستخدم رسالة صوتية". المستخدمون الآخرون الذين طلبوا نفس المسار هم مشتركون عاديون. يتم إرسال حالة التنزيل إليهم ، مثل المالك ، مرة واحدة كل 4 ثوانٍ تقريبًا حتى لا تتقاطع الرسالة (يتم تعليق الحالة لمدة 5 ثوانٍ). بمجرد اكتمال تنزيل المسار للمالك ، يتم استدعاء finish_callback () من الخدمة أعلاه. بعد ذلك ، يتم حذف جميع المشتركين من قائمة الحالة وتلقي المسار الذي تم تنزيله.

في الحل المعماري ، يكون مالك المسار مشتركًا أيضًا ، ولكن بعلامة معينة ، نظرًا لأن طرق إرسال المسار مختلفة.

5. التخزين المؤقت الاستعلام


كما نتذكر من مقالتي الأخيرة ، فإن الطلبات إلى Yandex.Music API ثقيلة جدًا. يمكن أن تكون قائمة المسارات 3 أو 5 ميغابايت. علاوة على ذلك ، هناك الكثير من الطلبات. مع كل عملية تحديث ، يتم إرسال طلبين على الأقل إلى Yandex: لتهيئة العميل وإجراء محدد. في بعض الأماكن ، لجمع معلومات كافية (على سبيل المثال ، لقائمة تشغيل) ، تحتاج إلى تقديم طلب لقائمة تشغيل ، لتلقي مساراتها ، للحصول على معلومات من الصفحة المقصودة (إذا كانت هذه قائمة تشغيل ذكية) ، ولا تنسى تهيئة العميل. بشكل عام ، رعب هادئ من حيث عدد الطلبات.

أردت شيئًا عالميًا جدًا ، ولم أقم بأي نوع من التخزين لأشياء معينة ، نفس العملاء.

نظرًا لأن المكتبة تسمح لك بتحديد المثيل الخاص بك لتنفيذ الاستعلام ، على رأس الطلباتثم استفدت من هذا.

النقطة بسيطة. فئة ذاكرة التخزين المؤقت نفسها مفردة. لديها معلمتان فقط: عمر التخزين المؤقت ، الحجم. عند تنفيذ الطلب ، يتم استدعاء الغلاف. تم تجاوزه. التحقق من ذاكرة التخزين المؤقت يحدث عن طريق تجزئة من الأقواس والمربعات المجمدة. ذاكرة التخزين المؤقت لديها الوقت لإضافتها. عند التحقق من ضرورة تحديث البيانات ، يتم الحصول على بيانات من LimitedSizeDict ، أو يتم تقديم طلب حقيقي وإضافته إلى ذاكرة التخزين المؤقت.

لا يمكن تخزين بعض الطلبات في ذاكرة التخزين المؤقت ، على سبيل المثال ، تعيين الإعجاب / عدم الإعجاب. إذا ضغط المستخدم على التسلسل التالي: مثل ، لا يعجبني ، مثل ، ثم في النهاية لن يتم تسليم الإعجاب. في مثل هذه الحالات ، عند إرسال طلب ، تحتاج إلى تمرير وسيطة use_cache بقيمة تساوي False. في الواقع ، هذا هو المكان الوحيد حيث لا يتم استخدام ذاكرة التخزين المؤقت.

بفضل هذا ، أقوم بالطلبات الأكثر جرأة حتى يتم تخزينها مؤقتًا. لا أحاول تقسيمها إلى أجزاء صغيرة ولا أحتاجها إلا للصفحة الحالية. أنا آخذ كل شيء دفعة واحدة ، وعند التبديل بين الصفحات ، لدي سرعة تحويل ضخمة (مقارنة بالنهج القديم).

بالنسبة لي ، تحولت فئة الطلبات المخبأة بشكل جميل وتم دمجها ببساطة.

مصدر
import copy
import time

from typing import Union
from collections import OrderedDict

import requests

from yandex_music.utils.request import Request as YandexMusicRequest


class LimitedSizeDict(OrderedDict):
    def __init__(self, *args, **kwargs):
        self.size_limit = kwargs.pop("size_limit", None)
        OrderedDict.__init__(self, *args, **kwargs)
        self._check_size_limit()

    def __setitem__(self, key, value):
        OrderedDict.__setitem__(self, key, value)
        self._check_size_limit()

    def _check_size_limit(self):
        if self.size_limit is not None:
            while len(self) > self.size_limit:
                self.popitem(last=False)


class CachedItem:
    def __init__(self, response: requests.Response):
        self.timestamp = time.time()
        self.response = response


class Cache:
    __singleton: 'Cache' = None

    def __init__(self, lifetime: Union[str, int], size: Union[str, int]):
        Cache.__singleton = self

        self.lifetime = int(lifetime) * 60
        self.size = int(size)

        self.storage: LimitedSizeDict = LimitedSizeDict(size_limit=int(size))

    def get(self, *args, **kwargs):
        hash_ = Cache.get_hash(*args, **kwargs)

        return self.storage[hash_].response

    def update(self, response: requests.Response, *args, **kwargs):
        hash_ = Cache.get_hash(*args, **kwargs)
        self.storage.update({hash_: CachedItem(response)})

    def need_to_fetch(self, *args, **kwargs):
        hash_ = Cache.get_hash(*args, **kwargs)
        cached_item = self.storage.get(hash_)

        if not cached_item:
            return True

        if time.time() - cached_item.timestamp > self.lifetime:
            return True

        return False

    @classmethod
    def get_instance(cls) -> 'Cache':
        if cls.__singleton is not None:
            return cls.__singleton
        else:
            raise RuntimeError(f'{cls.__name__} not initialized')

    @staticmethod
    def freeze_dict(d: dict) -> Union[frozenset, tuple, dict]:
        if isinstance(d, dict):
            return frozenset((key, Cache.freeze_dict(value)) for key, value in d.items())
        elif isinstance(d, list):
            return tuple(Cache.freeze_dict(value) for value in d)

        return d

    @staticmethod
    def get_hash(*args, **kwargs):
        return hash((args, Cache.freeze_dict(kwargs)))


class Request(YandexMusicRequest):
    def _request_wrapper(self, *args, **kwargs):
        use_cache = kwargs.get('use_cache', True)

        if 'use_cache' in kwargs:
            kwargs.pop('use_cache')

        if not use_cache:
            response = super()._request_wrapper(*args, **copy.deepcopy(kwargs))
        elif use_cache and Cache.get_instance().need_to_fetch(*args, **kwargs):
            response = super()._request_wrapper(*args, **copy.deepcopy(kwargs))
            Cache.get_instance().update(response, *args, **kwargs)
        else:
            response = Cache.get_instance().get(*args, **kwargs)

        return response



6. تتبع التعرف عن طريق الرسائل الصوتية


في البداية ، لم يكن هناك تفكير في إضافة هذا إلى البوت ، ولكن حدث موقف مثير للاهتمام. يحتوي برنامج التتبُّع على محادثة (يشار إليها في وصف برنامج التتبُّع). بعد مرور بعض الوقت ، لاحظت أن الناس يدخلون إليها ويرسلون رسائل صوتية مع الموسيقى. في البداية ، اعتقدت أنه كان نوعًا جديدًا من الرسائل غير المرغوب فيها بحيث قام شخص ما بالتعبئة وكان يمزح. ولكن ، عندما كان هؤلاء الأشخاص أقل من 10 أعوام وكان الجميع يفعلون نفس الشيء ، اقترح صديقي (نفس الشخص الذي اقترح للبنية ) أن يقوم المستخدمون بقيادة Yandex.Music في البحث ، في انتظار البوت الرسمي للتعرف على الموسيقى من Yandex ، يرون هناك غرفة دردشة ويرسلون إليها رسائل صوتية بكل جدية! لقد كان مجرد افتراض رائع وحقيقي. ثم قلت مازحا أن الوقت قد حان للاعتراف وإضافة بوت للدردشة. من أجل المتعة ... بعد فترة ، تم ذلك!

الآن عن API . في الآونة الأخيرة ، استخدمت Yandex بشكل متزايد مآخذ الويب. قابلت استخدامها في إدارة i.module ومحطة i. تعمل أيضًا خدمة التعرف على الموسيقى. لقد رميت الحد الأدنى من حلول العمل في برنامج الروبوت الخاص بي ، لكنني لم أضيف التنفيذ إلى المكتبة. يقع

WS على العنوان التالي: wss: //voiceservices.yandex.net/uni.ws
نحن بحاجة إلى رسالتين فقط - التفويض وطلب الاعتراف .
يرسل Yandex نفسه ، في تطبيقه الرسمي ، ملفات قصيرة في ثانية أو ثلاث. استجابة لذلك ، يمكنك الحصول على الحاجة إلى إرسال المزيد من البيانات أو النتيجة - تم العثور عليها أم لا. إذا تم العثور على النتيجة ، فسيتم إرجاع معرف المسار. يتم

إرسال ملفات Ogg معENCODER = SpeechKit Mobile SDK v3.28.0 . لم أتحقق من كيفية عملها مع برامج التشفير الأخرى ، ولكني قمت فقط بتغييرها في الملف المسجل بواسطة Telegram.

عند الرجوع باستخدام مقبس ويب ، حدث السحر في بعض الأحيان. في بعض الأحيان لم أتمكن من العثور على المسار ، ولكن عندما قمت بتغيير اللغة في الرسالة باستخدام طلب التعرف ، كنت أفعل. أو في البداية وجد ، ثم توقف ، على الرغم من أن الملف هو نفسه. اعتقدت أن لغة المسار يتم تعيينها من خلال SpeechKit على العميل. لا أملك هذه الفرصة للقيام بذلك بنفسي ، أقوم بعملية بحث عن القوة الغاشمة.

تنفيذي للركبة من التعرف على الصوت عن طريق الرسائل الصوتية من Telegram
import uuid

import lomond


def get_auth_data():
    return {
        "event": {
            "header": {
                "messageId": str(uuid.uuid4()),
                "name": "SynchronizeState",
                "namespace": "System"
            },
            "payload": {
                "accept_invalid_auth": True,
                "auth_token": "5983ba91-339e-443c-8452-390fe7d9d308",
                "uuid": str(uuid.uuid4()).replace('-', ''),
            }
        }
    }


def get_asr_data():
    return {
        "event": {
            "header": {
                "messageId": str(uuid.uuid4()),
                "name": "Recognize",
                "namespace": "ASR",
                "streamId": 1
            },
            "payload": {
                "advancedASROptions": {
                    "manual_punctuation": False,
                    "partial_results": False
                },
                "disableAntimatNormalizer": False,
                "format": "audio/opus",
                "music_request2": {
                    "headers": {
                        "Content-Type": "audio/opus"
                    }
                },
                "punctuation": False,
                "tags": "PASS_AUDIO;",
                "topic": "queries"
            }
        }
    }


class Recognition:
    URI = 'wss://voiceservices.yandex.net/uni.ws'
    LANGS = ['', 'ru-RU', 'en-US']
    POLL_DELAY = 0.3

    def __init__(self, binary_data, status_msg):
        self.status_msg = status_msg
        self.websocket = lomond.WebSocket(self.URI)
        self.chunks = self.get_chunks_and_replace_encoder(binary_data)

    def get_track_id(self):
        for lang in Recognition.LANGS:
            asr_data = get_asr_data()
            if lang:
                asr_data['event']['payload'].update({'lang': lang})
                self.status_msg.edit_text(f'     {lang}...')
            else:
                self.status_msg.edit_text(f'      ...')

            for msg in self.websocket.connect(poll=self.POLL_DELAY):
                if msg.name == 'ready':
                    self.websocket.send_json(get_auth_data())
                    self.websocket.send_json(asr_data)

                    for chunk in self.chunks:
                        self.websocket.send_binary(chunk)

                if msg.name == 'text':
                    response = msg.json.get('directive')

                    if self.is_valid_response(response):
                        self.websocket.close()

                        return self.parse_track_id(response)
                    elif self.is_fatal_error(response):
                        self.websocket.close()

                        break

    def is_valid_response(self, response):
        if response.get('header', {}).get('name') == 'MusicResult' and \
                response.get('payload', {}).get('result') == 'success':
            self.status_msg.edit_text(f' ,  !')
            return True
        return False

    @staticmethod
    def is_fatal_error(response):
        if response.get('header', {}).get('name') == 'MusicResult' and \
                response.get('payload', {}).get('result') == 'music':
            return False
        return True

    @staticmethod
    def parse_track_id(response):
        return response['payload']['data']['match']['id']

    @staticmethod
    def get_chunks_and_replace_encoder(binary_data):
        chunks = []

        for chunk in binary_data.split(b'OggS')[1:]:
            if b'OpusTags' in chunk:
                pos = chunk.index(b'OpusTags') + 12
                size = len(chunk)
                chunk = chunk[:pos] + b'#\x00\x00\x00\x00ENCODER=SpeechKit Mobile SDK v3.28.0'
                chunk += b"\x00" * (size - len(chunk))

            chunks.append(b'\x00\x00\x00\x01OggS' + chunk)

        return chunks


, .

7. الملاحظات الصغيرة


في البداية ، يمكن للمستخدمين الذين لديهم اشتراك فقط استخدام برنامج التتبُّع نظرًا لحقيقة أنه يمكن استخدام الخدمة في عدد محدود من البلدان (بدون اشتراك) ، ويقع الخادم مع برنامج التتبُّع في أوروبا. يتم حل المشكلة باستخدام وكيل لتنفيذ الطلبات من المستخدمين بدون اشتراك. يقع الخادم الوكيل في موسكو.

هناك اختيار من الصفحات ، لكنه يقتصر على 100 (لا يمكن إضافة المزيد من الأزرار ، تقييد Telegram). تحتوي بعض طلبات الصفحات الشائعة على المزيد من الصفحات.

في بحث Yandex ، يكون عدد العناصر في الصفحة مشفرًا. كم عدد المسارات وعدد قوائم التشغيل. في بعض الأحيان لا يتوافق هذا حتى مع كمية البيانات المعروضة على الجبهة. هناك تغيير في الصفحة ، عدد العناصر عائم. لذلك ، في البوت يقفز أيضًا ، وهو ليس جميلًا جدًا. والجمع بين مروِّحهم ورقمه - شيء من هذا القبيل. في الأماكن الأخرى حيث يمكن طلب عدد معين من العناصر في الصفحة ، كل شيء مثالي. الرسالة الصوتية بعد تنفيذ البحث في البوت هي t.me/MarshalC/416 .

عند إعادة توجيه الصوت في Telegram ، يتم فقد المؤلف وتعيينه لمن قام بإعادة التوجيه. لذلك ، يتم إرسال جميع المسارات بتوقيع اسم مستخدم الروبوت.

رسالة صوتية بكل ما التقيت به بعد تنفيذ الراديو في المكتبة - t.me/MarshalC/422(حول سلسلة المسارات ، من خلال إرسال كومة من الملاحظات ، batch_id ).

استنتاج


على الرغم من حقيقة أن هذه مقالة أخرى حول برنامج Telegram ، إلا أنك قرأتها حتى هنا ، على الأرجح لأنك كنت مهتمًا بأحد العناصر في المقطع وهذا رائع ، شكرًا جزيلاً لك !

لسوء الحظ ، لم أفتح شفرة المصدر للبوت بالكامل (لأنه في بعض الأماكن أحتاج إلى إعادة البناء). تم وصف الكثير في هذه المقالة ، ولكن بعض الجوانب ، على سبيل المثال ، مع لوحات المفاتيح الافتراضية وتوليدها لا تتأثر. بالنسبة للجزء الأكبر ، ما هو غير موجود في المقالة هو العمل فقط مع مكتبتي ، لا شيء مثير للاهتمام.

الطبقات التي يدور حولها كل شيء ، عرضتها في الشكل الذي هم فيه الآن. أعترف أن هناك أخطاء ، ولكن كل ذلك يعمل لفترة طويلة. في بعض الأماكن يعجبني الرمز الخاص بي ، وفي بعض الأماكن لا أحب - وهذا أمر طبيعي. لا تنس أن العمل مع WS من أجل الاعتراف هو حل على الركبة. جاهز لقراءة النقد المنطقي في التعليقات.

على الرغم من أن البوت تم التخطيط له حتى عندما بدأت في كتابة المكتبة ، فقد تبرأت من هذه الفكرة ، ولكن ، على ما يبدو ، عدت (كانت مملة).

Yandex.Music Bot - مشروع يثبت ملاءمة استخدام المكتبة للعمل مع API في المشاريع.

شكرا جزيلا لأمي ، يانا ، صنعاء ، المجد. شخص ما يقوم بتدقيق الأخطاء ، والبعض الآخر للتلميحات ، والتي بدونها ربما لم تكن هناك بعض النقاط في هذه المقالة ، وبالنسبة للبعض ببساطة لتقييم المقالة قبل النشر. آرثر عن picci للمقال ، Lyod للشعار.

PS لدي الآن مشكلة حادة في التوزيع بعد الدراسة. إذا كنت على استعداد لإجراء مكالمة علي - أخبرني أين أرسل سيرتك الذاتية ، قم بمقابلتي ، من فضلك. جميع جهات الاتصال الخاصة بي في ملف التعريف. التوقيت يحترق ، آمل لك. وفقا للقانون ، لا يمكنني العمل إلا في جمهورية بيلاروس.

PPS كانت هذه هي المقالة الثانية من ثلاثة ، ولكن بدون أدنى فكرة سيكون لدي الوقت للقيام بمشروع آخر حول هذا الموضوع وما إذا كانت هناك حاجة إليه. سأكشف علانية عن موضوعه - العميل عبر الأنظمة الأساسية Yandex.Music.

Source: https://habr.com/ru/post/undefined/


All Articles