Yandex.Music बॉट क्लाइंट के हुड के तहत

परिचय


हेलो, हेब्र! फिर, मैं Yandex.Music API को प्रभावित करने वाले दूसरे लेख के साथ हूं। मामले की योजना बनाई गई है और पहले लेख में उल्लेख किया गया है

हाथ पहुंचा, किया। आज मैं दिलचस्प के बारे में बात करूंगा, मेरी राय में, मेरे टेलीग्राम बॉट के कोडबेस में मौजूद क्षण, जो खुद को संगीत के पूर्ण ग्राहक के रूप में रखता है। हम यांडेक्स संगीत मान्यता एपीआई को भी स्पर्श करेंगे।

किसी विशेष चीज के कार्यान्वयन की एक बिंदु-दर-चरण कहानी शुरू करने से पहले, बॉट के बारे में और इसकी कार्यात्मक क्षमताओं के बारे में एक विचार होना सार्थक होगा।

ग्राहक वीडियो डेमो


मुख्य भाग में मैं निम्नलिखित के बारे में बात करूंगा:

  1. GitHub Pages (क्यों और क्यों) पर साइट के माध्यम से अपने खाते में साइन इन करें
  2. डेटा प्रारूप, इसकी पैकेजिंग और बटन डेटा में उपयोग।
  3. अपडेट राउटिंग, डेटा वर्जनिंग, संदर्भ को हैंडलर में फेंकना।
  4. सेवाएं:
    • टेलीग्राम में सेवा पुनः लोडिंग ट्रैक।
    • डाउनलोड की स्थिति भेजने के साथ एक ट्रैक प्राप्त करने के लिए "सदस्यता" की सेवा।
  5. सबसे सरल और सबसे सुरुचिपूर्ण क्वेरी कैशिंग कार्यान्वयन।
  6. वॉयस मैसेज द्वारा ट्रैक की पहचान और यह आम तौर पर बॉट में कैसे दिखाई देता है।
  7. छोटे नोट।

यदि आप कम से कम एक बिंदु में रुचि रखते हैं - बिल्ली में आपका स्वागत है।
बॉट उन उपयोगकर्ताओं के साथ काम करता है जिन्होंने अपने खाते में लॉग इन किया है, लेकिन नहीं। सब कुछ सेवा के चार मुख्य वर्गों के आसपास घूमता है: एल्बम, कलाकार, प्लेलिस्ट और ट्रैक। सभी इकाई डेटा समर्थित है, पृष्ठ चयन के साथ पृष्ठांकन है। उनके साथ बातचीत करने के लिए, एक अधिकृत उपयोगकर्ता अपने व्यक्तिगत मेनू का उपयोग कर सकता है, जिसमें शामिल हैं: स्मार्ट प्लेलिस्ट, "मुझे पसंद है" प्लेलिस्ट और अपने स्वयं के प्लेलिस्ट। अनधिकृत उपयोगकर्ताओं के लिए, खोज उपलब्ध है - एक नियमित संदेश के रूप में खोज क्वेरी भेजना। अब बस एक सूखी सूची है कि और क्या है: एक सीधा लिंक के माध्यम से पटरियों और अन्य वस्तुओं को प्राप्त करना, गीत प्राप्त करना, ट्रैक और कलाकार को पसंद / नापसंद करने की क्षमता, समान पटरियों को देखना, ध्वनि संदेश और अधिक द्वारा ट्रैक को पहचानना।

मुख्य हिस्सा


1. खाते में प्रवेश करें


प्रारंभ में, बॉट को प्राधिकरण के बिना काम करने की योजना नहीं थी, इसलिए, यहां तक ​​कि डिजाइन के दौरान, यह तय किया गया था कि प्राधिकरण कैसे होना चाहिए - अपनी वेबसाइट के माध्यम से । मुख्य तर्क सुरक्षा और पारदर्शिता थे। किसी भी मामले में स्पष्ट संदेश में उपयोगकर्ता लॉगिन और पासवर्ड स्वीकार नहीं करना चाहते थे। और उपयोगकर्ता के कंप्यूटर से प्राधिकरण प्राथमिक है। इसलिए, प्रतिक्रिया पर प्राधिकरण के लिए एक साइट लिखी गई थी । यह बॉट में पुनर्निर्देशित के साथ प्राधिकरण का एक रूप है। कैप्चा के प्राधिकरण और प्रसंस्करण के साथ एक टुकड़ा पायथन में पुस्तकालय से लिया गया था और जावास्क्रिप्ट में फिर से लिखा गया था

प्राधिकरण के लिए, प्राप्त OAuth टोकन का उपयोग किया जाता है, जिसे गहरी लिंकिंग के माध्यम से वापस बॉट में स्थानांतरित किया जाता है

पुनर्निर्देशन के लिए अंतिम लिंक इस तरह दिखता है: t.me/music_yandex_bot?start=elinesoauth_token}

मैं सबसे अच्छा चाहता था, लेकिन उपयोगकर्ताओं का भरोसा नहीं था। केवल प्रत्येक दसवां उपयोगकर्ता अधिकृत था (उस समय जब यह अनिवार्य था)। इसलिए, निम्नलिखित अपडेट के साथ, मुझे यह समझाना पड़ा कि आपको क्यों भरोसा करना चाहिए। HTTPS के बारे में सभी आइटम , विशेष रूप से ग्राहक पक्ष और खुले स्रोत कोड से अनुरोध के बारे में प्राधिकरण के लिए साइट पर प्रकाशित किए गए थे और बॉट में एक स्वागत संदेश में डुप्लिकेट किया गया था। बेहतर हो गया।

इसके अलावा, कम प्राधिकरण के कारकों के बीच, रूस में t.me के लिए दर्पणों की अयोग्यता निकली और केवल उपयोगकर्ताओं के लिए समर्थन के साथ समर्थन (नोटों में इस पर अधिक, पैरा 7)।

पहला आइटम URI ( tg: // ) का उपयोग करके हल किया गया था , और एक चुटकी में, पहले से ही दर्पण के लिए एक लिंक है (यदि स्वचालित रीडायरेक्ट काम नहीं करता है और बटन ने मदद नहीं की है), और दूसरा आइटम 7 में फिर से है।


2. डेटा प्रारूप


इस बॉट के लिए, मैंने पहली बार NoSQL DB - MongoDB का उपयोग करने का निर्णय लिया । समझ में आया: जब एक दस्तावेज़ एम्बेड करें, जब नहीं। मूंगोइन के साथ काम करना पसंद है। लेकिन मैं वास्तव में घर के सभी बटन डेटा को स्टोर नहीं करना चाहता, और क्लाइंट के पास केवल डेटाबेस में रिकॉर्ड आईडी है। मैं डेटा की मात्रा से घबरा गया था, लेकिन मैं डेटा भंडारण के लिए 512 एमबी की सीमा के साथ एक मुफ्त मोंगो सर्वर लेना चाहता था। रिकॉर्ड का एक सुंदर पुन: उपयोग करने के लिए अगर डेटा कई बटन में मेल खाता है और पुराने को साफ करने के लिए बटन में सब कुछ संग्रहीत करने की तुलना में बहुत अधिक कठिन है। संग्रहीत किए जाने वाले डेटा के आकार का विश्लेषण करने के बाद, मैंने निष्कर्ष निकाला कि यह आसानी से फिट बैठता है।

सबसे पहले, मैंने सिर्फ JSON का उपयोग किया, लेकिन उसने बहुत जल्दी से मना कर दिया जब वह एक सीमा में भाग गया। टेलीग्राम में, बटन डेटा की सामग्री केवल UTF-8 में 64 बाइट्स से अधिक नहीं हो सकती है

इसलिए, एक दोस्त के संकेत के साथ, मैंने स्ट्रक्चर मॉड्यूल से पैक को देखना शुरू कर दिया । तो प्रश्नों के प्रकार का जन्म हुआ, आदिम प्रारूप, पैकेजिंग और अनपैकिंग। अब इसका उपयोग बॉट में बिल्कुल हर जगह किया जाता है।

प्रारूप बहुत सरल है। पहला बाइट प्रकार है, दूसरा संस्करण है। बाकी सब कुछ एक विशेष प्रकार का डेटा है। प्रकारों को एनम के रूप में संग्रहीत किया जाता है, एक आईडी है , जो पहली बाइट है। आईडी के अलावा , प्रत्येक प्रकार में डेटा को पैकिंग और अनपैक करने के लिए एक प्रारूप है। उदाहरण के लिए: SHOW_TRACK_MENU टाइप करेंकिसके प्रारूप का मान "s?" है, जहां "s" ट्रैक का विशिष्ट पहचानकर्ता है, और "?" - क्या ट्रैक में टेक्स्ट है।

इस्तेमाल किया स्ट्रिंग प्रकार पटरियों पर क्योंकि: पहला, आईडी ट्रैक के का एक संयोजन हो सकता है आईडी और एल्बम आईडी पेट के माध्यम से ट्रैक के, और दूसरी, यह हो सकता UUID । साथ पटरियों UUID - आत्म लोड पटरियों, केवल उपयोगकर्ता के लिए जो उन्हें डाउनलोड करने के लिए उपलब्ध।

चूंकि डेटा हमेशा प्रारूप के अनुरूप नहीं होता है, उदाहरण के लिए, समान ट्रैक आईडी को केवल एक संख्या द्वारा दर्शाया जा सकता है, पैकिंग से पहले इसे प्रारूप के लिए एक प्रकार में डालना होगा। इस मामले में, एसइसलिए, कक्षा में एक ऐसा तरीका है जो पैकेजिंग के लिए स्थानांतरित डेटा को सामान्य करता है, ताकि निर्माणकर्ता को पास होने पर खुद ऐसा न करें।

स्ट्रिंग्स आत्मनिर्भर हैं और पैकेजिंग करते समय अपनी लंबाई का संकेत दे सकते हैं और इस लंबाई को ध्यान में रखते हैं।

पुराने संस्करणों के लिए समर्थन की योजना नहीं बनाई गई थी, इसलिए यदि संस्करण मेल नहीं खाते हैं तो एक अपवाद फेंक दिया जाता है। जब प्रसंस्करण अद्यतन, जिसे मैं अगले पैराग्राफ में चर्चा करूंगा, आवश्यक तर्क कहा जाता है।

चूंकि टेलीग्राम विशेष रूप से UTF-8 खाता है , इसलिए पैक किए गए डेटा को base85 में इनकोड किया गया हैहां, मैं यहां गति कम कर रहा हूं और बेस 64 का उपयोग किए बिना आकार में सबसे छोटी बचत कर रहा हूं, लेकिन छोटे आंकड़ों को देखते हुए, मैं बेस 85 का उपयोग उचित मानता हूं

स्रोत। फ़ाइल कॉलबैक_डेटा
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. रूटिंग अद्यतन और संदर्भ


प्रोजेक्ट टेलीग्राम बॉट एपीआई के साथ काम करने के लिए अजगर-टेलीग्राम-बॉट लाइब्रेरी का उपयोग करता हैइसमें पहले से ही कुछ प्रकार के अपडेट के लिए हैंडलर्स को पंजीकृत करने के लिए एक प्रणाली है, जो नियमित अभिव्यक्ति, कमांड और इतने पर फिल्टर के लिए आते हैं। लेकिन, अपने स्वयं के डेटा प्रारूप और मेरे प्रकारों को देखते हुए, मुझे TelegramHandler से विरासत में मिला और अपने हैंडलर को लागू करना पड़ा

अपडेट और संदर्भ तर्कों के माध्यम से प्रत्येक हैंडलर को दिए जाते हैं। इस मामले में, मेरा अपना संदर्भ है और यह हैंडलर में हैयह बनाया जा रहा है, और यह है: डेटाबेस में उपयोगकर्ता को प्राप्त करना और / या जोड़ना, संगीत तक पहुंच प्राप्त करने के लिए टोकन की प्रासंगिकता की जांच करना, Yandex.Music क्लाइंट को प्रारंभ करना, प्राधिकरण स्थिति और सदस्यता की उपलब्धता पर निर्भर करता है।

इसके अलावा मेरे से Handler'a वहाँ अधिक विशिष्ट संचालकों, उदाहरण के लिए कर रहे हैं CallbackQueryHandler । इसके साथ, एक हैंडलर एक निश्चित प्रकार के अद्यतन (मेरे प्रकार, डेटा प्रारूप के साथ) के लिए पंजीकृत है। वर्तमान हैंडलर के लिए यह अद्यतन उपयुक्त है या नहीं यह जाँचने के लिए, सभी डेटा अनपैक्ड नहीं है, लेकिन केवल पहले दो बाइट्स हैं। इस स्तर पर, कॉलबैक लॉन्च करने की आवश्यकता सत्यापित है। केवल तभी जब कॉलबैक को लॉन्च करना आवश्यक है - क्या डेटा पूरी तरह से अनपैक किया गया है और kwargs के रूप में स्थानांतरित किया गया हैअंतिम हैंडलर को। तुरंत ChatBase को विश्लेषणात्मक डेटा भेजना है

पंजीकरण क्रमिक रूप से होता है, और प्राथमिकता उन लोगों के लिए अधिक है जो पहले पंजीकृत होंगे (वास्तव में, Django रूटिंग के रूप में , और अन्य परियोजनाओं में)। इसलिए, अप्रचलित संस्करण के लिए हैंडलर का पंजीकरण करना CallBackQuery संचालकों में पहला है

पुराने संस्करण को संसाधित करने का तर्क सरल है - उपयोगकर्ता को इस बारे में सूचित करें और यदि संभव हो तो अद्यतन डेटा भेजें।

4. सेवाएँ


जब एक नियंत्रण वर्ग में बॉट को लॉन्च किया जाता है, तो सभी सेवाओं को आरंभीकृत किया जाता है, जो तब बॉट ( डीजे ) में कहीं भी सार्वभौमिक रूप से उपयोग किया जाता है

प्रत्येक सेवा के अपने स्वयं के थ्रेडपूल एक्ज़ीक्यूटर होते हैं, जिनमें कुछ वर्कर काम करते हैं।

टेलीग्राम में एक ट्रैक को पुनः लोड करना


फिलहाल, इस सेवा को टेलीग्राम में डाउनलोड की गई फ़ाइल के आकार की सीमा को दरकिनार करने के लिए उपयोगकर्ता बॉट को फिर से नहीं लिखा गया है । जैसा कि यह निकला, Yandex.Music में 50 mb - पॉडकास्ट से बड़ी फाइलें हैं।

सेवा फ़ाइल के आकार की जांच करती है और अधिकता के मामले में, उपयोगकर्ता के लिए अलर्ट फेंकती है। पैराग्राफ 5 में वर्णित कैशिंग सिस्टम के लिए धन्यवाद, यह गीत की उपलब्धता और प्राप्ति के लिए जांच करता है। ट्रैक भी कैश्ड हैं। फ़ाइल का हैश डेटाबेस में संग्रहीत है। यदि कोई है, तो ज्ञात कैश के साथ ऑडियो भेजा जा रहा है।

डेटाबेस में एक फ़ाइल की अनुपस्थिति में, Yandex.Music से एक सीधा लिंक प्राप्त होता है। हालांकि फिलहाल, उपयोगकर्ताओं के पास गुणवत्ता सेटिंग्स को बदलने की क्षमता नहीं है, लेकिन सभी मानक मानों पर सेट हैं। फ़ाइल को उपयोगकर्ता सेटिंग्स से बिटरेट और कोडेक द्वारा खोजा जाता है।

फ़ाइल और उसके कवर को tempfile.TemporaryFile () के रूप में डाउनलोड किया जाता है , जिसके बाद उन्हें टेलीग्राम पर अपलोड किया जाता है। यह ध्यान देने योग्य है कि टीजी हमेशा ट्रैक की अवधि को सही ढंग से नहीं पहचानता है, लेकिन मैं आमतौर पर कलाकार और शीर्षक के बारे में चुप रहता हूं। इसलिए, ये डेटा यैंडेक्स से लिए गए हैं, सौभाग्य से, उन्हें कार्ट में प्रसारित करना संभव है।

जब इस सेवा द्वारा एक ऑडियो फ़ाइल भेजी जाती है, तो end_callback () कहा जाता है , डाउनलोड के अंत के बारे में सदस्यता द्वारा सेवा का संकेत देता है।

ट्रैक प्राप्त करने और डाउनलोड स्थिति भेजने के लिए सदस्यता सेवा


ट्रैक तुरंत लोड नहीं किए जाते हैं। यह संभव है कि कई उपयोगकर्ताओं ने एक ही ट्रैक का अनुरोध किया हो। इस मामले में, पहले ट्रैक का अनुरोध करने वाला उपयोगकर्ता ट्रैक का आरंभकर्ता और स्वामी होता है। जब एक सेकंड से अधिक लोड हो रहा है, तो डाउनलोड स्थिति शुरू होती है: "उपयोगकर्ता एक आवाज संदेश भेजता है"। अन्य उपयोगकर्ता जिन्होंने समान ट्रैक का अनुरोध किया वे नियमित ग्राहक हैं। उन्हें, मालिक की तरह, डाउनलोड स्टेटस हर ~ 4 ​​सेकंड में एक बार भेजा जाता है ताकि संदेश बाधित न हो (स्टेटस 5 सेकंड के लिए हैंग हो जाए)। जैसे ही मालिक के लिए ट्रैक का डाउनलोड पूरा हो जाता है, तैयार_कोलबैक () ऊपर की सेवा से कहा जाता है। उसके बाद, सभी ग्राहकों को स्थिति सूची से हटा दिया जाता है और डाउनलोड किया गया ट्रैक प्राप्त होता है।

वास्तु समाधान में, ट्रैक का मालिक भी एक ग्राहक है, लेकिन एक निश्चित चिह्न के साथ, चूंकि ट्रैक भेजने के तरीके अलग हैं।

5. क्वेरी कैशिंग


जैसा कि हम अपने पिछले लेख से याद करते हैं, Yandex.Music API के लिए अनुरोध बहुत भारी हैं। पटरियों की सूची 3 या 5 एमबी हो सकती है। इसके अलावा, बस बहुत सारे अनुरोध हैं। प्रत्येक अद्यतन प्रसंस्करण के साथ, कम से कम 2 अनुरोध Yandex को भेजे जाते हैं: ग्राहक को आरम्भ करने के लिए और एक विशिष्ट कार्रवाई के लिए। कुछ स्थानों में, पर्याप्त जानकारी एकत्र करने के लिए (उदाहरण के लिए, किसी प्लेलिस्ट के लिए), आपको प्ले ट्रैक के लिए, लैंडिंग पृष्ठ (यदि यह एक स्मार्ट प्लेलिस्ट है) से जानकारी के लिए, और ग्राहक के प्रारंभिककरण के बारे में मत भूलना। सामान्य तौर पर, अनुरोधों की संख्या के संदर्भ में शांत भय।

मैं कुछ बहुत सार्वभौमिक चाहता था, और कुछ वस्तुओं के लिए किसी भी तरह का भंडारण नहीं करता था, वही ग्राहक।

चूंकि लाइब्रेरी आपको क्वेरी निष्पादन के लिए अपने स्वयं के उदाहरण को अनुरोध के शीर्ष पर निर्दिष्ट करने की अनुमति देती है, तब मैंने इसका फायदा उठाया।

बात सरल है। कैश क्लास अपने आप में एक सिंगलटन है। इसके केवल दो पैरामीटर हैं: कैश जीवनकाल, आकार। जब अनुरोध निष्पादित किया जाता है, तो आवरण को कहा जाता है। वह ओवरराइड हो गया है। जमे हुए आर्ग और क्वार्ट्स के हैश द्वारा कैश की जाँच करना। कैश में जोड़ने का समय है। डेटा को अपडेट करने की आवश्यकता की जांच करते समय, या तो LimitedSizeDict से डेटा प्राप्त किया जाता है, या एक वास्तविक अनुरोध किया जाता है और कैश में जोड़ा जाता है।

कुछ अनुरोधों को कैश नहीं किया जा सकता है, उदाहरण के लिए, जैसे / नापसंद की स्थापना। यदि उपयोगकर्ता निम्नलिखित अनुक्रम को दबाता है: जैसे, नापसंद, पसंद, तो अंत में पसंद वितरित नहीं किया जाएगा। ऐसे मामलों के लिए, जब कोई अनुरोध भेजते हैं, तो आपको उपयोग_साचे तर्क को गलत के बराबर मान के साथ पास करना होगादरअसल, यह एकमात्र ऐसी जगह है जहां कैश का उपयोग नहीं किया जाता है।

इसके लिए धन्यवाद, मैं सबसे साहसिक अनुरोध करता हूं ताकि उन्हें कैश किया जाए। मैं इसे छोटे लोगों में तोड़ने की कोशिश नहीं कर रहा हूं और केवल वर्तमान पृष्ठ के लिए इसकी जरूरत है। मैं सब कुछ एक ही बार में लेता हूं, और जब पृष्ठों के बीच स्विच करता हूं तो मेरे पास एक बड़ी स्विचिंग गति होती है (पुराने दृष्टिकोण की तुलना में)।

मेरे लिए, कैश्ड अनुरोध वर्ग खूबसूरती से बदल गया और बस एकीकृत हो गया।

स्रोत
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 ऐसे लोग थे और हर कोई एक ही काम कर रहा था, तो मेरे दोस्त (उसी ने संरचना से सुझाव दिया) ने सुझाव दिया कि यूंडेक्स से संगीत की पहचान के लिए आधिकारिक बॉट की तलाश में यूंडेक्स.म्यूजिक ड्राइव करें। वे वहाँ एक गपशप कक्ष देखते हैं और पूरी गंभीरता से उसमें संदेश भेजते हैं! यह सिर्फ एक शानदार और सच्ची धारणा थी। तब मैंने मज़ाक में कहा कि यह मान्यता बनाने और चैट में बॉट जोड़ने का समय था। मनोरंजन के लिए ... थोड़ी देर के बाद, यह किया गया था!

अब एपीआई के बारे में । हाल ही में, यैंडेक्स ने तेजी से वेब सॉकेट्स का उपयोग किया है। मैं एक i.odule और एक i.station के प्रबंधन में उनके उपयोग से मिला। संगीत मान्यता सेवा भी इस पर काम करती है। मैंने अपने बॉट में न्यूनतम काम करने वाले समाधान को फेंक दिया, लेकिन मैंने पुस्तकालय में कार्यान्वयन को नहीं जोड़ा।

WS निम्न पते पर स्थित है: wss: //voiceservices.yandex.net/uni.ws
हमें केवल दो संदेशों की आवश्यकता है - प्राधिकरण और पहचान के लिए अनुरोध
यैंडेक्स खुद, अपने आधिकारिक आवेदन में, एक या तीन सेकंड में छोटी फाइलें भेजता है। जवाब में, आप अधिक डेटा या परिणाम भेजने की आवश्यकता प्राप्त कर सकते हैं - पाया या नहीं। यदि परिणाम पाया जाता है, तो ट्रैक आईडी वापस कर दी जाएगी। .के साथ फाइल

भेजी जाती हैंENCODER = भाषण मोबाइल एसडीके v3.28.0मैंने यह नहीं जांचा कि यह अन्य एन्कोडर के साथ कैसे काम करता है, मैं इसे टेलीग्राम द्वारा रिकॉर्ड की गई फ़ाइल में बदल देता हूं।

जब एक वेब सॉकेट के साथ उलट, कभी-कभी जादू हुआ। कभी-कभी मुझे ट्रैक नहीं मिलता था, लेकिन जब मैंने मान्यता अनुरोध के साथ संदेश में भाषा बदल दी, तो मैंने किया। या पहले यह पाया गया, और फिर यह बंद हो गया, हालांकि फ़ाइल समान है। मुझे लगा कि ट्रैक की भाषा उनके भाषण क्लाइंट द्वारा निर्धारित की गई है इसे स्वयं करने का ऐसा अवसर नहीं होने के कारण, मैं एक क्रूर बल खोज करता हूं।

टेलीग्राम से आवाज संदेशों द्वारा आवाज पहचान का मेरा घुटना बनाया हुआ कार्यान्वयन
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 तक सीमित है (कोई और बटन नहीं जोड़ा जा सकता है, टेलीग्राम प्रतिबंध)। कुछ सामान्य पृष्ठ अनुरोधों में बहुत अधिक पृष्ठ होते हैं।

यैंडेक्स खोज में, पृष्ठ पर तत्वों की संख्या हार्डकोड है। कितने ट्रैक, कितने प्लेलिस्ट। कभी-कभी यह सामने की ओर प्रदर्शित डेटा की मात्रा के अनुरूप भी नहीं होता है। एक पृष्ठ परिवर्तन है, तत्वों की संख्या तैर रही है। इसलिए, बॉट में यह भी कूदता है, जो बहुत सुंदर नहीं है। और उनके साथ अपने पेजिनेटर को संयोजित करने के लिए - ऐसा कुछ। अन्य स्थानों में जहां प्रति पृष्ठ तत्वों की एक निश्चित संख्या का अनुरोध करना संभव है, सब कुछ सही है। बॉट में खोज को लागू करने के बाद आवाज संदेश t.me/MarshalC/416 है

जब टेलीग्राम में फ़ॉरवर्ड ऑडियो, लेखक खो जाता है और उसे उसी को सौंपा जाता है जिसने फ़ॉरवर्ड किया था। इसलिए, सभी ट्रैक बॉट उपयोगकर्ता नाम के हस्ताक्षर के साथ भेजे जाते हैं।

लाइब्रेरी में रेडियो के कार्यान्वयन के बाद मुझे जो कुछ भी मिला, उसके साथ आवाज संदेश - t.me/MarshalC/422(पटरियों की श्रृंखला के बारे में, प्रतिक्रिया के एक बैच भेजने के साथ इसके माध्यम से जा रहा है, बैच_आईडी )।

निष्कर्ष


इस तथ्य के बावजूद कि यह टेलीग्राम बॉट के बारे में एक अन्य लेख है, आपने इसे यहीं पढ़ा, सबसे अधिक संभावना है क्योंकि आप कट में से एक अंक में रुचि रखते थे और यह अद्भुत है, बहुत-बहुत धन्यवाद !

दुर्भाग्य से, मैंने पूरी तरह से बॉट के स्रोत कोड को नहीं खोला (क्योंकि कुछ जगहों पर मुझे रिफ्लेक्टर की आवश्यकता है)। इस लेख में बहुत कुछ वर्णित किया गया है, लेकिन कुछ पहलुओं, उदाहरण के लिए, वर्चुअल कीबोर्ड और उनकी पीढ़ी प्रभावित नहीं होती है। अधिकांश भाग के लिए, लेख में जो नहीं है वह सिर्फ मेरे पुस्तकालय के साथ काम कर रहा है, कुछ भी दिलचस्प नहीं है।

जिन कक्षाओं के आसपास सब कुछ घूमता है, मैंने उस रूप में दिखाया जिसमें वे अब हैं। मैं मानता हूं कि कीड़े हैं, लेकिन यह लंबे समय तक काम करता है। कुछ जगहों पर मुझे अपना कोड पसंद है, कुछ जगहों पर मैं नहीं करता - और यह सामान्य है। यह मत भूलो कि मान्यता के लिए WS के साथ काम करना घुटने पर एक समाधान है। टिप्पणियों में तर्कपूर्ण आलोचना पढ़ने के लिए तैयार।

हालाँकि, जब मैंने पुस्तकालय लिखना शुरू किया था तब भी बॉट की योजना बनाई गई थी, तब मैंने इस विचार को अस्वीकार कर दिया था, लेकिन, जाहिर है, मैं लौट आया (यह उबाऊ था)।

Yandex.Music बॉट - एक परियोजना जो परियोजनाओं में एपीआई के साथ काम करने के लिए पुस्तकालय का उपयोग करने की उपयुक्तता साबित करती है

माँ, याना, सना, महिमा को बहुत धन्यवाद। गलतियाँ फैलाने के लिए कोई, संकेत के लिए कुछ, जिसके बिना इस लेख में कुछ बिंदुओं का अस्तित्व नहीं था, और कुछ बस प्रकाशन से पहले लेख का मूल्यांकन करने के लिए। लेख के लिए पिक्सी के लिए आर्थर, लोगो के लिए लयोड।

पुनश्च अब मेरे पास अध्ययन के बाद वितरण के साथ एक तीव्र मुद्दा है। यदि आप मुझ पर कॉल करने के लिए तैयार हैं - मुझे बताएं कि सीवी कहां भेजें, मुझे साक्षात्कार दें, कृपया। मेरे सभी संपर्क प्रोफ़ाइल में हैं। समय जल रहा है, मैं तुम्हारे लिए आशा करता हूं। कानून के अनुसार, मैं केवल बेलारूस गणराज्य में ही काम कर सकता हूं।

पीपीएस यह तीन का दूसरा लेख था, लेकिन बिना किसी मामूली विचार के मेरे पास इस विषय पर एक और परियोजना करने का समय होगा और क्या इसकी आवश्यकता है। मैं सार्वजनिक रूप से अपने विषय - क्रॉस-प्लेटफ़ॉर्म क्लाइंट Yandex.Music को प्रकट करूँगा।

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


All Articles