विषय पर बढ़ें

प्लगइन API संदर्भ

वह अनुबंध जिसके हिसाब से प्लगइन लिखा जाता है: plugin.yaml के फ़ील्ड, वे दो gRPC सर्विस जिनसे बैकएंड और सर्वर आपस में संवाद करते हैं, और window.evey bridge, जो इंटरफ़ेस मॉड्यूल को उसके फ़्रेम में मिलता है। इनका इस्तेमाल करने वाला पूरा उदाहरण प्लगइन लिखना में है; हर अनुमति क्या करने देती है और इंस्टॉलर क्या मना करता है, यह अनुमतियाँ और स्लॉट में है।

Manifest

हर ZIP के root फ़ोल्डर में एक plugin.yaml (या plugin.yml) होता है, ज़्यादा से ज़्यादा 1 MB का।

plugin.yaml
name: my-plugin              # ज़रूरी; अनोखा, kebab-case
version: 1.2.3               # ज़रूरी; semver, X.Y.Z या X.Y.Z-label
kind: plugin                 # ज़रूरी; "plugin" या "theme"
language: nodejs             # प्लगइन के लिए ज़रूरी; इमेज का कोई runtime
entrypoint: node index.js    # प्लगइन के लिए ज़रूरी; वह कमांड लाइन जो सर्वर चलाता है

description: "..."           # वैकल्पिक
author: "..."                # वैकल्पिक
min_evey_version: "1.0.0"    # वैकल्पिक; स्वीकार होता है और अनदेखा किया जाता है

permissions:                 # बैकएंड और इंटरफ़ेस क्या कॉल कर सकते हैं
  - network
  - media.read
external_fetch:              # "network" के साथ ज़रूरी; सिर्फ़ होस्ट के नाम
  - api.themoviedb.org
capabilities:                # वे भूमिकाएँ जो प्लगइन निभाने का दावा करता है
  - metadata.provider

web_root: public/            # frontend बताया हो तो ज़रूरी; static फ़ाइलों के रूप में दिया जाता है
frontend:
  - slot: home.section       # अनुमति वाले स्लॉट में से एक
    src: public/front.js     # web_root के अंदर होना चाहिए

config_schema:               # प्लगइन के कार्ड पर कॉन्फ़िग पैनल के फ़ील्ड
  api_key:
    type: password           # किसी भी क्लाइंट को कभी वापस नहीं भेजा जाता
    label: API key           # दिखाया नहीं जाता: पैनल फ़ील्ड को उनकी key से नाम देता है
    required: true
  city:
    type: string
    default: Paris
    description: Resolved through geocoding
फ़ील्ड नियम
name, version, kind ज़रूरी। version X.Y.Z या X.Y.Z-label जैसा होना चाहिए; अपडेट का वर्ज़न इंस्टॉल किए गए वर्ज़न से ऊँचा होना चाहिए।
language kind: plugin के लिए ज़रूरी: python, nodejs, php, ruby, lua, bash, sh या go। इससे तय होता है कि कौन सा runtime जाँचा जाए और कौन से stub entrypoint के बगल में कॉपी हों; यह कुछ शुरू नहीं करता।
entrypoint kind: plugin के लिए ज़रूरी। पहला शब्द interpreter है (या native-binary के साथ binary), बाकी उसके arguments; प्रोसेस प्लगइन के फ़ोल्डर में चलता है।
permissions हर मान अनुमतियों की सूची में होना चाहिए। network के लिए external_fetch खाली नहीं होना चाहिए।
external_fetch होस्ट के नाम। scheme और अंत का path पार्स के समय हटा दिए जाते हैं; होस्ट ठीक-ठीक मिलने चाहिए, *.example.com जैसा wildcard इंस्टॉल पर मना हो जाता है और port का suffix अनदेखा किया जाता है; IP लिखने पर इंस्टॉल मना होता है (नियम)।
capabilities metadata.provider, media.read, media.write, webhook.receiver या notification.sender; बाकी सब मना है। थीम कोई क्षमता नहीं बता सकती।
web_root frontend खाली न हो तो ज़रूरी; relative, बिना .., बिना backslash। . प्लगइन का पूरा फ़ोल्डर देता है और सिर्फ़ एक चेतावनी लॉग करता है।
frontend[] slot स्लॉट की सूची में होना चाहिए; src web_root के अंदर होना चाहिए।
config_schema फ़ील्ड के नाम से {type, label, description, required, default, min, max, options} तक का map; label दिखाया नहीं जाता, पैनल फ़ील्ड को उनकी key से नाम देता है। टाइप: string, text, password, boolean, integer, number, select (options: [{value, label}] के साथ)।

थीम (kind: theme) language, entrypoint, permissions या capabilities नहीं बता सकती; उसका फ़ॉर्मैट थीम में है।

Secret। type: password वाला फ़ील्ड, या secret: true वाला फ़ील्ड, सामान्य तरीके से सहेजा जाता है और बैकएंड को दिया जाता है, पर पढ़ने वाले हर route से छिपा हुआ लौटता है, एडमिन का कॉन्फ़िग पैनल भी। पैनल को छिपे हुए मान को बिना बदले सहेजने पर सहेजा हुआ मान बना रहता है, इसलिए किसी दूसरे फ़ील्ड को बदलने से secret कभी नहीं मिटता।

बैकएंड: दो सर्विस

बैकएंड प्रोसेस और सर्वर दो Unix socket पर gRPC से संवाद करते हैं, जिनके रास्ते environment में मिलते हैं:

EVEY_PLUGIN_ID=<plugin id>
EVEY_PLUGIN_SOCK=<path>   # आपका प्रोसेस यहाँ सुनता है और PluginService देता है
EVEY_HOST_SOCK=<path>     # आपका प्रोसेस HostService को कॉल करने के लिए यहाँ जुड़ता है

सर्वर अपने socket पर प्रोसेस को उस PID से पहचानता है जो kernel बताता है (SO_PEERCRED); प्रोसेस को कोई token नहीं दिया जाता। हर प्लगइन के लिए कॉल 600 प्रति मिनट तक सीमित हैं। proto/plugin/v1/plugin_api.proto अनुबंध है; साथ आने वाली भाषाओं के stub प्लगइन शुरू होने पर entrypoint के बगल में कॉपी हो जाते हैं (पहले से क्या चाहिए)।

PluginService: सर्वर क्या कॉल करता है

Method कब
Initialize(plugin_id, config, host_version) जब प्रोसेस अपने socket से जुड़ जाए, एक बार। config में config_schema के वे मान होते हैं जो एडमिन ने सेट किए। शुरू होने से मना करने के लिए error के साथ success: false लौटाएँ।
HealthCheck() समय-समय पर, ताकि चलता हुआ प्रोसेस अटके हुए प्रोसेस से अलग पहचाना जा सके। जब तक आप जवाब दे सकते हैं, healthy: true लौटाएँ।
Shutdown(reason) जब प्लगइन अक्षम, अपडेट या अनइंस्टॉल हो, या सर्वर रुके। जवाब देने के बाद जल्दी बंद हो जाएँ।
OnEvent(event_type, payload, source) अनुबंध में है, पर सर्वर आज प्लगइन को कोई event नहीं भेजता: इसे handled: true लौटाने के लिए लागू करें, और कोई कॉल न आने की उम्मीद रखें।
GetCapabilities() अनुबंध में है; सर्वर आज इसे कॉल नहीं करता।

HostService: प्लगइन क्या कॉल कर सकता है

जिस कॉल की अनुमति manifest में नहीं बताई गई, वह PermissionDenied के साथ मना हो जाती है। दिया नहीं जाता वाले method Unimplemented लौटाते हैं; वे आगे के किसी वर्ज़न के अनुबंध का हिस्सा हैं, और यहाँ इसलिए लिखे हैं ताकि किसी stub से आपको गलतफ़हमी न हो।

Method अनुमति टिप्पणी
Log(level, message, fields) कोई नहीं प्लगइन के लॉग में लिखा जाता है, जो उसके कार्ड पर लॉग में दिखता है।
GetConfig(key) / SetConfig(key, value) कोई नहीं प्लगइन का कॉन्फ़िगरेशन: कॉन्फ़िग पैनल के मान, और वे कुंजियाँ जो प्लगइन खुद लिखता है। ज़्यादा से ज़्यादा 256 कुंजियाँ, 256 बाइट की कुंजी, 16 KiB का मान।
GetData, SetData, DeleteData, ListDataKeys कोई नहीं नीचे वाला डेटा स्टोर।
EmitEvent(event_type, payload) कोई नहीं सर्वर की event bus पर plugin.event.<event_type> के रूप में प्रकाशित होता है, जहाँ वेबहुक इसकी सदस्यता ले सकते हैं।
GetMediaItem(id), ListMediaItems(library_id, type, parent_id, limit, offset) api.library या media.read मीडिया का पूरा रिकॉर्ड, file_path, provider_ids और genres के साथ।
Fetch(url, method, headers) network सैंडबॉक्स से बाहर जाने का अकेला रास्ता: http या https, GET या HEAD, external_fetch का कोई होस्ट; पता निकालने के बाद और redirect में भी निजी पते मना हैं। body 8 MiB तक सीमित है (truncated यह बताता है) और पूरा request 20 सेकंड तक। सर्वर अपनी तरफ़ से कोई credential नहीं जोड़ता।
SearchMedia, GetMediaStreams, GetChapters, GetSubtitles api.library या media.read दिया नहीं जाता।
UpdateMetadata api.metadata या media.write दिया नहीं जाता।
GetUserPreferences api.user दिया नहीं जाता।
SendNotification notification.sender दिया नहीं जाता।

डेटा स्टोर

हर प्लगइन के पास एक स्थायी key/value स्टोर होता है, बिना कोई अनुमति बताए: cursor, cache, आखिरी sync का समय, कुछ भी जो restart के बाद भी याद रखना हो। मान ऐसी string हैं जिन्हें सर्वर नहीं पढ़ता; JSON खुद encode करें। प्लगइन की id पहचाने गए socket से आती है, इसलिए प्लगइन सिर्फ़ अपने हिस्से में पढ़ता और लिखता है, और पूरा स्टोर प्लगइन के साथ मिटा दिया जाता है।

Method काम
SetData(key, value) मान बनाना या बदलना।
GetData(key) value और found लौटाता है।
DeleteData(key) कुंजी हटाना।
ListDataKeys() प्लगइन की कुंजियाँ।

यह स्टोर पूरे प्लगइन के लिए एक है। हर उपयोगकर्ता का डेटा (विजेट देखने वाले व्यक्ति की कोई पसंद) इंटरफ़ेस की तरफ़ रहता है, नीचे दी गई prefs अनुमति के ज़रिए।

Frontend API

इंटरफ़ेस मॉड्यूल एक opaque origin वाले फ़्रेम में चलता है, default-src 'none' नीति के साथ: होस्ट पेज, उसकी कुकी या उसके स्टोरेज तक कोई पहुँच नहीं, कोई फ़ॉर्म नहीं, कोई popup नहीं, और connect-src सिर्फ़ https पर external_fetch के होस्ट तक सीमित। बाकी सब window.evey से होकर जाता है, जो मॉड्यूल चलने से पहले डाला जाता है। वही मॉड्यूल बिना बदले वेब ऐप में और मोबाइल और टीवी ऐप में चलता है।

evey.onInit(callback)

होस्ट जो context भेजता है, उसके साथ एक बार कॉल होता है; context आने के बाद रजिस्टर किया गया callback तुरंत कॉल होता है।

evey.onInit(function (data) {
  // data.config          - प्लगइन के कॉन्फ़िगरेशन मान (config_schema)
  // data.context         - होस्ट का संदर्भ
  // data.locale          - चालू भाषा, जैसे "en-US"
  // data.theme           - "dark" या "light"
  // data.surface         - "web", "mobile" या "tv"
  // data.suggestedHeight - सिर्फ़ home.section: होस्ट की सुझाई ऊँचाई (px)
  // data.viewportHeight  - सिर्फ़ tv: पैनल की ऊँचाई, dp में
});

evey.navigate(path)

होस्ट ऐप्लिकेशन को path पर ले जाता है। सिर्फ़ /home, /library, /media, /player, /settings और /search से शुरू होने वाले रास्ते माने जाते हैं; बाकी सब (एडमिन वाला हिस्सा, साइन-इन, पूरा URL) चुपचाप छोड़ दिया जाता है।

evey.toast(level, message)

होस्ट इंटरफ़ेस में एक toast दिखाता है। level "success", "error" या "info" है; message सादा टेक्स्ट है, 200 अक्षरों पर काटा जाता है; एक सेकंड में ज़्यादा से ज़्यादा 3 toast दिखते हैं।

evey.resize(height)

स्लॉट को height पिक्सेल ऊँचा करने को कहता है, 50 और 800 के बीच रखा जाता है। 0 स्लॉट को समेट देता है: जिस विजेट के पास दिखाने को कुछ नहीं, वह जगह नहीं लेता। home.section विजेट बिना कुछ किए data.suggestedHeight पर खुलता है: होस्ट जानता है कि उसने आपको किस जगह रखा है और वहाँ होम विजेट की जो ऊँचाई होती है वही सुझाता है, ताकि हर विजेट एक ही आकार में दिखे। आकार तभी बदलें जब आपके पास इससे बेहतर जानकारी हो।

evey.prefs.* (अनुमति prefs)

मौजूदा उपयोगकर्ता की पसंद, प्लगइन के अपने हिस्से में, उस token के ज़रिए (यह प्लगइन, यह उपयोगकर्ता) से जुड़ी हुई जो होस्ट फ़्रेम के लिए रखता है। हर कॉल एक promise लौटाती है; अनुमति न हो तो वह reject होता है।

await evey.prefs.set("units", "celsius");
const { value, found } = await evey.prefs.get("units");
const all = await evey.prefs.list();        // [{ key, value }]
await evey.prefs.delete("units");

evey.media.* (अनुमति public)

मीडिया आइटम की एक सुरक्षित झलक: id, टाइटल, टाइप, साल, सारांश, पोस्टर का URL। कभी फ़ाइल का रास्ता, आकार, provider id या stream नहीं।

const item  = await evey.media.get(id);
const page  = await evey.media.list({ libraryId });
const data  = await evey.media.poster(id);  // पोस्टर, data: URL के रूप में

poster एक data: URL लौटाता है, क्योंकि फ़्रेम की नीति सर्वर से सीधे तस्वीर लोड करने से मना करती है; इसे <img> के स्रोत या texture की तरह इस्तेमाल करें।

evey.assets.read(path)

प्लगइन की अपनी web_root की कोई फ़ाइल, relative रास्ते से, data: URL के रूप में पढ़ता है: जो फ़ाइल बैकएंड ने चलते समय लिखी, वह cross-origin की चिंता के बिना फ़्रेम में इस्तेमाल हो सकती है। सिर्फ़ प्लगइन की अपनी फ़ाइलें पहुँच में हैं।

टीवी पर

टीवी ऐप मॉड्यूल को एक WebView में चलाता है, और कुछ बातें जो ब्राउज़र में सच हैं, वहाँ सच नहीं रहतीं:

  • अपने viewport से आकार तय न करें। WebView के अंदर window.innerHeight वह स्लॉट है जो होस्ट ने अभी आपको दिया। आकार data.viewportHeight या data.suggestedHeight से तय करें।
  • न focus, न pointer। विजेट को कभी D-pad का focus नहीं मिलता और hover नहीं होता; जो भी interactive है, वह वेब और मोबाइल के लिए है।
  • टेक्स्ट किनारों से दूर रखें। टीवी overscan करते हैं: हर तरफ़ चौड़ाई और ऊँचाई का लगभग 5% खाली छोड़ें।
  • WebGL असली डिवाइस पर परखें। shader डिवाइस के GPU और driver पर चलता है, आपके कंप्यूटर पर नहीं; noise के coordinates सीमित रखें। tests/plugins/weather-v2 ऊपर की सभी बातों के लिए नमूने वाला विजेट है।

स्लॉट

स्लॉट कहाँ दिखता है
home.section होम पेज पर कंटेंट का एक सेक्शन
home.hero होम पेज का सबसे ऊपर वाला बड़ा बैनर
library.sidebar लाइब्रेरी वाले पेजों में साइड पैनल
media.detail.sidebar मीडिया के जानकारी वाले पेज पर साइड पैनल
player.overlay वीडियो प्लेयर के ऊपर एक परत
settings.user.appearance उपयोगकर्ता सेटिंग्स का रूप-रंग वाला हिस्सा
sidebar.bottom मुख्य नेविगेशन साइडबार का निचला हिस्सा
admin.nav एडमिन मेन्यू में एक एंट्री, जो प्लगइन का अपना पेज खोलती है

एडमिन वाले हिस्से में admin.nav अकेला स्लॉट है। यह मौजूदा एडमिन स्क्रीन में कुछ नहीं डालता: यह एक मेन्यू एंट्री जोड़ता है जो प्लगइन का पेज उसी सैंडबॉक्स वाले फ़्रेम में खोलती है, एक ऐसे route पर जो सिर्फ़ एडमिन के लिए है। इसे माँगना मंज़ूरी वाले फ़र्क में दिखता है। बाकी हर admin.*, login.*, setup.* और profile.security.* स्लॉट इंस्टॉल के समय मना होता है।

क्षमताएँ

क्षमता वह भूमिका बताती है जो प्लगइन निभाने का दावा करता है: metadata.provider, media.read, media.write, webhook.receiver या notification.sender। इंस्टॉलर नाम जाँचता है, एडमिन पूर्वावलोकन में सूची देखता है और अपडेट पर हर नई क्षमता देखता है, और सुरक्षा डैशबोर्ड उसे दर्ज करता है। सर्वर आज किसी क्षमता के आधार पर कुछ नहीं भेजता: metadata.provider को जानकारी जोड़ने का कोई कॉल नहीं मिलता, webhook.receiver को कोई आने वाला request नहीं मिलता। प्लगइन असल में क्या कर सकता है, यह उसकी अनुमतियाँ तय करती हैं।

इंटरफ़ेस की अनुमतियाँ

public और prefs बाकी अनुमतियों की तरह permissions में बताई जाती हैं। ये बैकएंड प्रोसेस को कुछ नहीं देतीं; ये फ़्रेम में evey.media.* और evey.prefs.* खोलती हैं। फ़्रेम के पास कभी token नहीं होता: होस्ट (प्लगइन, उपयोगकर्ता) तक सीमित पाँच मिनट का एक token बनाता है और कॉल आगे भेजता है, और सर्वर डिफ़ॉल्ट रूप से हर वह method मना करता है जिसका नाम token के दायरे में नहीं है (ADR-0012)।

प्लगइन कभी क्या नहीं माँग सकता

admin.*, auth.*, system.*, user.write और library.write पूरी तरह बंद हैं: इनका नाम लेने वाला manifest मना होता है, कोई token इन्हें कभी नहीं ले जा सकता, और एडमिन method प्लगइन token को मना करते हैं, उसका दायरा कुछ भी हो।

उदाहरण

examples/plugins/ में मौजूदा manifest फ़ॉर्मैट वाले चलते प्लगइन हैं: Node.js और Python में शुरुआती प्लगइन (nodejs-example, python-example), एक मेटाडेटा provider (metadata-example), एक वेबहुक receiver (webhook-receiver), build step वाला एक होम विजेट (weather), public और prefs अनुमतियों और admin.nav स्लॉट के साथ डेटा स्टोर (data-demo), config_schema के हर फ़ील्ड टाइप का उदाहरण, admin.nav पेज के साथ (config-demo), और दो थीम (theme-dark-blue, open-workshop)। make plugin-zips इन सबको पैक करता है।