m3u plus مقابل m3u: السمات التي تضيفها قائمة تشغيل ممتدة، وكيف تحلّلها

لا توجد مواصفة لـ M3U+. هناك اصطلاح، ومجموعة عيّنات، ومحلِّل عليه أن ينجو من الاصطلاح والعيّنات معًا.

#EXTINF:-1 tvg-id="" tvg-logo="" group-title="News;General" tvg-chno="1",Example One
https://provider.example.com/stream/one.m3u8

سطران. هذا الصف مركَّب لا منسوخ: قائمتا التشغيل اللتان نحتفظ بهما لعمل المحلِّل تسمّيان جهات بث حقيقية، فلا شيء هنا مأخوذ منهما حرفيًا. وكل شكل فيه شكلٌ تحتويه تلك الملفات فعلًا، وليست أي من القيم مميِّزة. في الملف ذي الـ 1,895 مُدخَلًا، 107 صفوف تدّعي القناة رقم 1، و20 صفًا تحمل tvg-id فارغة، و42 صفًا تحمل tvg-logo فارغة. وفي الملف ذي الـ 308 مُدخَلات، 25 صفًا تحمل group-title خالية، و19 صفًا تحمل tvg-logo فارغة، و10 صفوف تحشر اسمَي مجموعتين في سمة واحدة بفاصلة منقوطة.

كل ما يبدو على السطر الأول بيانات مهيكلة يكاد يكون غائبًا أو خاليًا أو مشتركًا مع مئة قناة لا صلة بينها. والحقل الوحيد الحاضر بموثوقية والقريب من التمايز هو العنوان في السطر الثاني: 308 عناوين متمايزة عبر 308 مُدخَلات في الملف الأصغر، و1,884 عبر 1,895 في الأكبر.

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

لا توجد مواصفة، بل يوجد اصطلاح

يعرّف RFC 8216 الوسم #EXTINF في HLS على أنه مدة، ثم فاصلة، ثم عنوان اختياري مقروء للبشر يمتد إلى نهاية السطر. ولا يقول شيئًا عن tvg-id ولا tvg-chno ولا group-title ولا tvg-logo ولا catchup-source، ولا عن أي من الرموز الأخرى التي تحشرها الملفات الحقيقية بين المدة والفاصلة.

هذه الرموز تأتي من اصطلاح لاحق لقوائم تشغيل التلفزيون المباشر ليس له اسم متفق عليه، والاسمان اللذان يلجأ إليهما الناس كلاهما خطأ. فـ Extended M3U هو اصطلاح عصر Winamp الذي أدخل #EXTM3U و#EXTINF نفسيهما؛ ويستشهد به RFC 8216 بالاسم في القسم 4.3.1.1، وهو لا يحتوي شيئًا من tvg- أصلًا. أما M3U+ فليس صيغةً من الأساس: m3u_plus وضع إخراج في خوادم المزوّدين التي تصل إليها ببيانات دخول المزوّد، وهو قيمة الاستعلام التي تحوّل التصدير من عناوين مجردة إلى صفوف تحمل tvg-id وtvg-logo وgroup-title. أحدهما اصطلاح قوائم تشغيل من تسعينيات القرن الماضي، والآخر معامل استعلام، وليس أيٌّ منهما مواصفة. والاصطلاح تعرّفه المُصدِرات بما تُصدِره.

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

line     := "#EXTINF:" WS* duration WS* attributes "," displayName
duration := "-"? DIGIT+

مطابقة البادئة ترتيبية، فـ #EXTINF: حساسة لحالة الأحرف حتى النقطتين الرأسيتين، والمحلِّل يثبّت طول البادئة عند ثمانية محارف قبل أن يسلّم الباقي إلى ماسح المدة. وذلك الماسح يأخذ إشارة ناقص مفردة اختيارية، ثم يشترط رقمًا واحدًا على الأقل، ثم يستهلك سلسلة الأرقام. ويسجّل موضع انتهاء الأرقام ثم يرمي القيمة. المدة لا تُقرأ في أي موضع من خط المعالجة. وكل أسطر #EXTINF البالغة 2,203 في قائمتَي التشغيل العيّنتين تحمل -1، وهي طريقة الاصطلاح في قول «حيّ، بلا طول معلوم»، وما من مسار شيفرة يعنيه لو قالت 7200.

ثلاث نتائج تخرج مباشرة من ذلك الماسح. +1 مرفوضة، لأن الزائد ليس ناقصًا ولا رقمًا. و.5 مرفوضة، لأن الرقم يجب أن يسبق النقطة. أما المدة الكسرية مثل #EXTINF:120.5,Example فتُحلَّل تحليلًا صحيحًا بالمصادفة: يتوقف الماسح عند النقطة، وتقع .5 المتبقية داخل منطقة السمات، ولا يطابقها أي بحث عن سمة أبدًا. يسمح RFC 8216 بذلك الشكل الكسري ومحلِّلنا يتعامل معه، لكن لم يبرمجه أحد، وهذا يستحق أن يُقال صراحةً بدل حفظه تحت بند «ندعم الأعداد العشرية».

في تلك القواعد عدم تماثل يستحق الاعتراف به، وإن لم يكن في الاتجاه الذي يبدو أول وهلة. بادئة الوسم تُطابَق بحساسية لحالة الأحرف، وهو ما يشترطه RFC 8216 في القسم 4.1 على كل وسم يبدأ بـ #EXT. أما أسماء السمات فتُطابَق بلا حساسية لحالة الأحرف، وهذا لا يشترطه شيء. لذا يُتخطّى #extinf: بحروفه الصغيرة بوصفه توجيهًا غير معروف، بينما تُحلَّل TVG-CHNO = "18" على السطر ذاته بلا مشكلة، والنصف المخالف للـ RFC هو النصف المتساهل. جاء هذا التضارب مع نقل عن محلِّل أقدم كان يعمل على الخادم، وملف المصدر ما زال يقول ذلك في أعلاه. ولا واحدة من قائمتَي التشغيل العيّنتين تحتوي وسمًا بحروف صغيرة، ولا اسم سمة بحروف كبيرة، ولا مسافة حول علامة المساواة، فلم يمرّ شيء في مجموعة عيّناتنا على أي من نصفَي هذا التضارب.

الفاصلة التي ليست محدِّدًا

قسّم سطر #EXTINF عند أول فاصلة فيه، وتكون قد كسرت 30 مُدخَلًا من 308 في أحد ملفَينا.

والسبب أن قيم السمات سلاسل مقتبسة، والسلاسل المقتبسة تحتوي فواصل. فكل صف من تلك الصفوف الـ 30 يحمل سمة user-agent فيها سلسلة وكيل مستخدم لمتصفح، وكل الـ 30 سلسلة تحتوي المقطع KHTML, like Gecko. ثمانية وعشرون منها تضعه بين قوسين، واثنتان جُرّدتا من القوسين في مكان ما قبلنا، وهذا لا يغيّر شيئًا في أمر الفاصلة. وهذه هي المقارنة الفعلية، مُجراة على الملف الحقيقي، مع حذف أسماء مضيفات المصدر واستبدال اسم القناة نفسه:

naive first-comma split -> like Gecko) Chrome/145.0.0.0 Safari/537.36 Edg/145.0.0.0",Example News (720p)
quote-aware split       -> Example News (720p)

تسعة فاصلة سبعة بالمئة من ذلك الملف تحصل على اسم عرض مصنوع من سلسلة إصدار متصفح شخص ما. والكتالوج يُبنى رغم ذلك. ولا شيء يخطئ. وقائمة القنوات خاطئة على نحو يبدو خللًا في العرض وليس كذلك.

العلاج ماسح مكتوب باليد، لا تعبير نمطي: امشِ في منطقة السمات محرفًا محرفًا، واقلب راية inQuotes عند كل علامة اقتباس مزدوجة، وأعِد أول فاصلة تراها بينما تلك الراية مطفأة. كل ما قبلها منطقة السمات، وكل ما بعدها اسم العرض، وكلاهما مقصوص الأطراف. هي تسعة أسطر من الشيفرة، وهي أعلى تسعة أسطر قيمةً في المحلِّل.

وذلك الماسح هو أيضًا من يقرر ما يحدث للصف المشوَّه، والقرار صارم. فإذا حملت منطقة السمات عددًا فرديًا من علامات الاقتباس المزدوجة، مثل group-title="Movies,Example Four حيث سقطت علامة الإغلاق، فلن تعود الراية إلى الإطفاء أبدًا، ولن يُعثر على فاصلة خارج الاقتباس، ويُرفض المُدخَل كله مع تشخيص. لا ارتداد إلى أول فاصلة، ولا محاولة لتخمين موضع علامة الاقتباس الناقصة. اخترنا الرفض على الإصلاح لأن الصف المُصلَح خاطئ بصمت، أما الصف المرفوض فغائب بصوت عالٍ، والصف الغائب بصوت عالٍ هو الذي يستطيع القارئ الإبلاغ عنه.

خمس سمات تُقرأ، وملف واحد يحمل 1,895 صفًا من سادسة نتجاهلها

مجمل السمات التي يبحث عنها محلِّلنا خمس تهجئات:

  • group-title، يُحتفظ بها حتى أول فاصلة منقوطة، والخالية تنطوي إلى لا شيء
  • tvg-logo، مقصوصة الأطراف
  • logo، لا يُرجَع إليها إلا حين تكون tvg-logo غائبة تمامًا
  • tvg-id، تُؤخذ خامًا وقت التحليل ويُفحص وثوقها لاحقًا
  • tvg-chno، تُسلَّم إلى محلِّل أرقام يُسمح له بألا يعيد شيئًا

هذه هي القائمة كلها. أسماء السمات تُطابَق بلا حساسية لحالة الأحرف، مع مسافات اختيارية على جانبَي علامة المساواة، فـ TVG-CHNO = "18" وtvg-chno="18" سواء عندنا. والقيم يجب أن تكون بين علامتَي اقتباس مزدوجتين؛ لا مسار لقيمة بلا اقتباس، وعلامات الاقتباس المفردة غير معروفة في أي موضع.

والآن الجانب المرصود، وهو الأمتع. في الملف ذي الـ 1,895 مُدخَلًا، تظهر tvg-name في الصفوف الـ 1,895 كلها ونتجاهلها كلها. وتظهر tvg-country في 1,642 صفًا ونتجاهلها هي الأخرى. اسم العرض يأتي دائمًا من النص الذي بعد الفاصلة، لا من tvg-name أبدًا، حتى حين يقدّم مصدر كليهما في كل صف ينشره. وهذا قرار تصميمي لا سهو: العنوان الذي بعد الفاصلة هو الحقل الذي تضمنه القواعد النحوية نفسها، وtvg-name اختيارية، ومصدر اسم واحد ذو حجية أفضل من ترتيب أفضليات يتصرف تصرفًا مختلفًا مع كل ملف.

والملف الأصغر يُظهر طبقة التوجيهات التي تقع بين سطرَي المُدخَل. فهو يحمل 30 سمة user-agent، و2 سمة referrer (يكتبها ذلك المُصدِر براءَين، وهي ليست تهجئة ترويسة HTTP)، و32 سطر توجيه #EXTVLCOPT منفصلًا، 30 منها تكرر وكيل المستخدم الذي ذكره سطر #EXTINF أصلًا. ولا نقرأ أيًا من هذه.

مشغّلات أخرى تقرأ سمات لا نقرأها، وما من قائمتين متطابقتين بين قوائمها. فإضافة IPTV Simple لـ Kodi توثّق catchup وcatchup-source وcatchup-days وtvg-shift وradio وتوجيه #EXTGRP. وSS-IPTV، وهو مشغّل للتلفزيونات الذكية، يوثّق aspect-ratio التي لا يوثّقها Kodi. وtvg-language جاءت من iptv-org، التي أسقطتها منذ ذلك الحين مع tvg-country. ولا يظهر أي من هذه الرموز في عيّنتنا، ولا نقرأ أيًا منها، وينتج عن ذلك قيد منتج صادق يستحق الذكر: إعادة البث عندنا موجودة، لكنها مبنية على بيانات دخول المزوّد، فمصدر M3U لا يستطيع أن يعرض علينا إعادة بث مهما كتب من سمات.

سبع ترويسات، وثلاث قنوات

هذا مقطع بُني ليمرّ على كل المسارات دفعة واحدة. كل مضيف فيه نطاق أمثلة محجوز، وكل اسم مخترع. ومعظم الأشكال التي يمرّ عليها تقع فعلًا في مجموعة عيّناتنا، ومنها BS4K101، وهي قيمة حقيقية مأخوذة من الملف الأكبر. وخمسة منها لا تقع فيها إطلاقًا: المدة المشوَّهة، وعلامة الاقتباس غير المتوازنة، وlogo المجردة، واسم السمة بحروف كبيرة مع مسافات حول علامة المساواة، والاسم الذي يبدأ بـ ##. والمقطع المبني يدويًا هو السبيل الوحيد لبلوغ تلك المسارات الخمسة، ويجدر أن تعرف ذلك قبل أن تقرأ الجولة أدناه على أنها دليل عن ملفات حقيقية.

#EXTM3U x-tvg-url="https://provider.example.com/guide.xml"
#EXTINF:-1 tvg-id="one.example" TVG-CHNO = "22.1" group-title="News;General" tvg-logo="",Example One
https://provider.example.com/stream/one.m3u8
#EXTINF:-1 tvg-id="two.example" user-agent="Mozilla/5.0 (KHTML, like Gecko)",Example Two
#EXTVLCOPT:http-user-agent=Mozilla/5.0 (KHTML, like Gecko)
https://provider.example.com/stream/two.m3u8
#EXTINF:-1 tvg-chno="BS4K101" group-title="Local",## ===== LOCAL =====
https://provider.example.com/stream/divider
#EXTINF:x tvg-chno="7" group-title="Movies",Example Three
https://provider.example.com/stream/three.m3u8
#EXTINF:-1 group-title="Movies,Example Four
https://provider.example.com/stream/four.m3u8
#EXTINF:-1 tvg-chno="9",Example Five
rtmp://provider.example.com/stream/five
#EXTINF:-1 logo="https://provider.example.com/logos/six.png",Example Six
https://provider.example.com/stream/six.m3u8

تدخل سبع ترويسات. ويخرج من المحلِّل أربعة صفوف، يبلغ الكتالوج ثلاثة منها، وتُنتَج خمس سلاسل تشخيص.

السطر 1 يُتخطّى. محلِّلنا لا يبحث عن #EXTM3U إطلاقًا؛ بل يسقط في مسار التخطي العام «يبدأ بعلامة مربّع» مع كل ما لا يعرفه، ولا تُقرأ سمة x-tvg-url التي عليه. ويشترط RFC 8216 تلك الترويسة أول سطر في قائمة التشغيل. نحن لا نشترطها، والملف الذي لا ترويسة له ويفتتح مباشرةً بـ #EXTINF يُحلَّل بلا مشكلة، لأن فحص الشكل الذي يجري على الجهاز قبل التحليل يقبل أيًا من الوسمَين أول شيء في الملف، بعد علامة ترتيب بايت اختيارية ومسافات بادئة. ولا يجوز أن يسبقه شيء آخر: الملف الذي يفتتح بسطر تعليق أو بـ #EXTVLCOPT يُرفض قبل أن يراه المحلِّل أصلًا.

Example One يُحلَّل باسم سمة بحالة أحرف خاطئة ومسافات حول علامة المساواة. تصير مجموعته News، لأن المقطع الذي قبل أول فاصلة منقوطة هو وحده ما يُحتفظ به. ويصير رقمه 22.1 عددًا عشريًا، لا 22 عددًا صحيحًا، وهذا يهمّ فيما يلي. ويصير شعاره سلسلة فارغة حيث تتوقع لا شيء البتة، وهذا أيضًا يهمّ فيما يلي.

Example Two هو حالة الفاصلة داخل الاقتباس، وسطر #EXTVLCOPT الذي يليه هو السبب في أن التخطي العام لعلامة المربّع يجب ألا يمسح الترويسة المعلَّقة. فالتوجيهات تقع بحق بين #EXTINF وعنوانه. والمحلِّل الذي يعيد ضبط حالته عند كل سطر # غير معروف سيُسقط هذه القناة وكل قناة مثلها، وهي في ملف العيّنة الأصغر 30 مُدخَلًا من 308.

صف الفاصل يُحلَّل بنظافة، وهو قناة في نظر القواعد النحوية. وقيمة tvg-chno فيه، BS4K101، يرفضها محلِّل الأرقام فتصير لا شيء، فيضيع الرقم لا الصف. أما ما يزيله فهو اسمه الذي يبدأ بـ ##، وذلك يحدث لاحقًا، أثناء تطبيع الأسماء، لا وقت التحليل. وفصل المرحلتين يعني أن المحلِّل لا رأي له في معنى اسم القناة، وهذا هو الموضع الصحيح لذلك الحدّ.

Example Three يفشل عند المدة: x ليس ناقصًا ولا رقمًا، فتُرفض الترويسة مع تشخيص وتُمسح الحقول المعلَّقة الخمسة كلها. ثم يصل عنوانه بلا ترويسة معلَّقة فينتج تشخيصًا ثانيًا. ترويسة مشوَّهة واحدة تكلّف سطرين وتنتج سطرَي سجل، وهذا صحيح ويجدر معرفته حين تقرأ تشخيصات استيراد حقيقي.

Example Four هو علامة الاقتباس غير المتوازنة، ويتصرف تصرفًا مطابقًا: تشخيصان، ولا شيء يُصدَر.

Example Five ترويسته صحيحة تمامًا وعنوانه rtmp. وهذا معرّف موارد مطلق، فيُحلَّل على أنه كذلك، لكن مخططه ليس http ولا https فنرفضه. وهو أخطر صف في المقطع، لأن مسار الرفض لو أخفق في مسح الحالة المعلَّقة لورث Example Six رقم القناة 9 بصمت. والمسار ليس افتراضيًا. فملف العيّنة الأكبر ينهي 6 من مُدخَلاته الـ 1,895 بعنوان rtmp أو rtsp، خمسة وواحد، وتلك الصفوف الـ 6 هي الصفوف الوحيدة في مجموعة العيّنات كلها البالغة 2,203 مُدخَلات التي يرفضها محلِّلنا.

Example Six يحمل سمة logo مجردة بلا tvg-logo وبلا group-title إطلاقًا، فيعمل الارتداد إلى الشعار وتحطّ القناة في مجموعة افتراضية تحمل اسم المصدر.

كل مسار تخطٍّ عليه أن يمسح الترويسة

مُدخَل M3U سطران يتشاركان حالة قابلة للتغيير، وتلك هي صعوبة الصيغة كلها. يحتفظ المحلِّل بخمسة حقول معلَّقة بين قراءة ترويسة وقراءة عنوان: الاسم، والمجموعة، والشعار، والمعرّف، والرقم. وسبعة مسارات متمايزة تجري في الحلقة، وصحة المحلِّل بأسرها مسألة ما يفعله كل مسار منها بتلك الخمسة.

الأسطر الفارغة تُتخطّى بلا مسح، وهذا صحيح، لأن سطرًا فارغًا بين ترويسة وعنوانها لا ينبغي أن يهدم المُدخَل. وتوجيهات # غير المعروفة تُتخطّى بلا مسح، للسبب الذي يبيّنه المقطع أعلاه. والترويسة المشوَّهة تمسح الخمسة كلها. والعنوان المرفوض يمسح الخمسة كلها. والإصدار الناجح يمسح الخمسة كلها.

المسار السابع هو الذي تخطئ فيه معظم محلِّلات الأسطر المزدوجة، والطريف أنه لا يُعالَج بإعادة ضبط أصلًا. فحين تصل ترويسة #EXTINF ثانية قبل أي عنوان، يُسنِد المحلِّل الحقول الخمسة كلها بلا شرط، بلا if وبلا إسناد بدمج القيم الفارغة. والبحث عن سمة لا يجد شيئًا يعيد لا شيء، ومحلِّل الأرقام إذا أُعطي لا شيء أعاد لا شيء، فالترويسة التي لا تحمل tvg-chno تكتب الفراغ فوق رقم الترويسة السابقة بدل أن تتركه مكانه. لا يوجد مسار يبقى به الحقل N من المُدخَل K إلى المُدخَل K+1. وهذه خاصية أقوى من «نتذكر أن نعيد الضبط»، ويستحق الأمر أن تُبنى بنية المحلِّل هكذا عن قصد، لأن نسخة إعادة الضبط تظل صحيحة ما دام كل تعديل مستقبلي يتذكر أن يوسّعها.

ومسار واحد يستحق ملاحظة لأنه يبدو خللًا وليس كذلك. مسار «وصل عنوان بلا ترويسة معلَّقة» يُصدر تشخيصه ولا يمسح شيئًا. وهذا آمن بحكم البناء لا بحكم الحظ، والثابت قابل للفحص بقراءة الإسنادات: الموضع الوحيد الذي تُضبط فيه الحقول الأربعة الأخرى هو مسار الترويسة، وذلك المسار يضبط الاسم دائمًا على قيمة غير فارغة في المرور نفسه، مرتدًّا إلى السلسلة الحرفية Unnamed channel حين يكون العنوان خاليًا. فالاسم الفارغ يعني أن الأربعة الأخرى فارغة أصلًا، وإعادة الضبط الغائبة عمل لا سبيل إلى بلوغه.

كل مسار من هذه المسارات يحتاج اختبار انحدار خاصًا به، والتسعة التي عندنا تغطي فعلًا مسارات التسرب: واحد يؤكد أن مُدخَلًا مرقَّمًا لا يسرّب رقمه إلى الذي يليه، وآخر يؤكد أن الترويسة المشوَّهة لا تفعل ذلك أيضًا. أما ما لا اختبار له إطلاقًا فهو ماسح الفاصلة داخل الاقتباس، أي الأسطر التسعة التي سمّتها هذه المقالة أعلى تسعة أسطر قيمةً في المحلِّل، ومعه رفض علامات الاقتباس غير المتوازنة، وحالة #EXTVLCOPT بين السطرين، والارتداد إلى logo المجردة. ولا شيء في تلك الفجوة يرمي استثناءً. والكتالوج يُبنى. ومئة قناة خلف صف واحد سيئ تحمل المجموعة الخطأ.

الحقلان اللذان يبدوان معرّفَين

tvg-chno بيانات عرض، والقياس الذي يحسم ذلك يستغرق سطرًا واحدًا من أوامر الطرفية. في الملف ذي الـ 1,895 مُدخَلًا، 1,355 مُدخَلًا تحمل السمة. وتلك الأرقام الـ 1,355 تغطي 263 قيمة متمايزة. و107 مُدخَلات تدّعي الرقم 1. والتكرار ليس أثرًا جانبيًا لدمج المجموعات، لأنه ينجو من التجميع: مجموعة واحدة في ذلك الملف تضم 7 قنوات كلها برقم 1، و33 من المجموعات الـ 85 التي تحمل أي رقم فيها تكرار واحد على الأقل. وذلك الملف فيه 92 مجموعة إجمالًا، فـ 7 منها لا تحمل أرقامًا إطلاقًا. والملف الآخر، 308 مُدخَلات، لا tvg-chno فيه في أي موضع. لا يجوز أن يُفهرَس شيء على هذه القيمة، ولا أن يفترض شيء أنها فريدة، وكل مستهلك يرتّب بها يحتاج فاصل تعادل حتميًا.

وتحليلها تمرين صغير في رفض الذكاء الزائد. تُقرأ القيمة عددًا بفاصلة عائمة تحت الثقافة الثابتة، لا عددًا صحيحًا، ولا تحت لغة الجهاز أبدًا. و56 من الـ 1,355 مرفوضة رفضًا مباشرًا. وليس واحد من الـ 56 رقميًا:

  • 49 منها رموز أنظمة بثّ: 7 تمتد من D011 إلى D081، و19 في عائلة من BS101 إلى BS4K101، و22 في سلسلة من CS055 إلى CS363، ورمز واحد لمشغّل كابل
  • 4 منها المحرف # حرفيًا، وهو على ما يبدو عنصر نائب لم يملأه المُصدِر قط
  • 3 منها نص حر مدفون فيه رقم، اثنان منها CH 106 وCH 108، وواحد اسم خدمة باليابانية ينتهي بـ 527

ويحمل المحلِّل أيضًا سقفًا قدره 99999، وما فوقه تُعامَل القيمة على أنها نفاية. ولم يعمل قط على مجموعة العيّنات هذه. وأكبر رقم في الملف هو 4999، وهو تذكير مفيد بأن حارسًا كُتب لفرضية ليس دليلًا على أن الفرضية كانت حقيقية.

والتحليل العشري ليس خيارًا أسلوبيًا. 25 مُدخَلًا في ذلك الملف تستخدم ترقيم القنوات الفرعية على طريقة ATSC، بـ 24 قيمة متمايزة تمتد من 22.1 إلى 36.2، وتحليلها عددًا صحيحًا يصهر 22.1 و22.2 و22.3 في ثلاث قنوات كلها برقم 22. والثقافة الثابتة ليست خيارًا أسلوبيًا هي الأخرى: العمل بلغة الجهاز سيجعل الملف نفسه ينتج أرقامًا مختلفة على جهاز مضبوط على الفاصلة علامةً عشرية، وهذا صنف من الأخطاء لا يتكرر إلا على حاسوب شخص آخر.

tvg-id أسوأ، لأن إخفاقاتها ليست فراغًا. في الملف ذي الـ 1,895 مُدخَلًا، السمة غائبة عن 483 صفًا، وفارغة في 20 صفًا آخر، وحاضرة لكنها تفشل في قاعدة الشكل عندنا في 24 صفًا إضافيًا، فينتهي 527 صفًا، أي 27.8 بالمئة من الملف، بلا معرّف صالح للاستخدام. وتلك الـ 24 هي المزعجة. سبعة عشر منها تبدو تمامًا كمعرّفات حقيقية، اسم قصير ثم نقطة ثم رمز بلد، ومرفوضة لسبب واحد لا غير، هو أنها أقصر من ستة محارف. والـ 7 الباقية لا نقطة فيها. الحد الأدنى للطول قاعدة فظّة، وهي تكلّفنا تلك الـ 17، وسنعيد النظر في العتبة لو نما العدد يومًا.

وسبب وجود تلك القاعدة أصلًا مجموعة عيّنات أخرى، نحتفظ بها لعمل المحلِّل، مصنوعة من صادرات مزوّدين لا من الملفين المنتقيَين أعلاه. ففي مصدرين من مصادرها الثلاثة كانت أشيع قيمة لـ tvg-id على الإطلاق هي السلسلة الحرفية TS: 446 صفًا في أحدهما و274 في الآخر. وTS هي صيغة حاوية تدفق النقل، تسرّبت من حقل صيغة الإخراج إلى عمود المعرّف. وتلك القنوات الـ 720 لا صلة بينها، قناة ترفيه بريطانية وقناة راديو ناطقة بالعربية جنبًا إلى جنب تحت معرّف واحد. ولأن مفتاح القناة مشتق من المعرّف، انهارت الـ 720 كلها في قناة منطقية واحدة. والنتيجة عنصر في المفضلة يظهر على قنوات لم يعلّمها أحد، وقفل أبوي يقفل 720 شيئًا دفعة واحدة، وخانة واحدة في العرض المتعدد تنوب عنها جميعًا.

وكان العلاج قاعدة ثقة لها موطن واحد، تُطبَّق عند حدّ الإدخال حتى لا يضطر شيء بعدها إلى تذكّر إعادة الفحص. المعرّف صالح للاستخدام فقط إذا كان بين 6 و128 محرفًا، وفيه نقطة، وليس TS، وما ينجو يُحوَّل إلى حروف صغيرة تحت الثقافة الثابتة، لأن المصادر تختلف اختلافًا حقيقيًا في حالة أحرف معرّف القناة الواحدة. وكل ما عدا ذلك يُطبَّع إلى لا شيء، و«لا معرّف» حالة يتعامل معها كل مستهلك أصلًا، لأن معظم القنوات لا معرّف لها فعلًا. والقاعدة ضرورية وغير كافية، ونحن نعلم ذلك: معرّف واحد اجتاز فحص الشكل تبيّن أنه مرتبط بـ 74 قناة لا صلة بينها، فحيثما تُجمَّع المصادر يجري فوقها فحص عدديّة وقت البناء. ولا واحد من الملفين المنتقيَين في عيّنتنا يحتوي معرّف TS واحدًا، وهذا هو التحفظ الصادق على كل ما سبق. مجموعة عيّنات نظيفة لن تريك هذا الإخفاق أبدًا. علمنا به من بلاغ خلل، لا من اختبار.

ومع غياب معرّف صالح في معظم الملفات، لا بد من اشتقاق واحد، والاشتقاق هو موضع المقايضة المزعجة. عنوان البث هو الحقل الوحيد الحاضر دائمًا والقريب من التفرد داخل المصدر الواحد، فالمعرّف المُصطنَع هو SHA-1 لذلك العنوان، بأول أربعة بايتات تُقرأ بترتيب البايت الأصغر أولًا، مطوية في عدد صحيح موجب بـ 32 بت. ويترك ذلك ما يقارب 2^31 من فضاء المفاتيح، وبحسب حدّ عيد الميلاد (birthday bound) فإن قائمة تشغيل بـ 50,000 مُدخَل تحمل احتمالًا يناهز 44 بالمئة لتصادم داخلي واحد على الأقل. قبلنا ذلك الرقم والحساب أمام أعيننا، على أساس أن التصادم يدمج قناتين داخل مصدر واحد، وهو ما يحدث أصلًا حين يُدرج مصدرٌ القناةَ نفسها مرتين. ومجموعة عيّناتنا تفعل ذلك في 9 عناوين تغطي 20 صفًا من 1,895 في الملف الأكبر، ولا مرة واحدة في الملف ذي الـ 308 مُدخَلات، فهو حدث حقيقي لا حدث معتاد، وذلك القياس يضعف الحجة أكثر مما يدعمها. وما يحسم المسألة فعلًا أن توسيع الأساس ليشمل tvg-id، أو الانتقال إلى دالة تجزئة أعرض، غير قابل للشحن: المعرّف المشتق يُكتب في بيانات محفوظة تقرأها أجهزة أخرى، فتغيير الاشتقاق يبطلها كلها دفعة واحدة. ومعدل التصادم هو ثمن صيغة سلكية مجمَّدة.

الخالي ليس غائبًا، إلا في حقل واحد

ثلاث سمات، وثلاث سياسات مختلفة للسلسلة الفارغة، ولم ننتبه إلا حين قسنا.

group-title="" تنطوي إلى لا شيء، لأن مُطبِّع المجموعات يعامل الخالي معاملة الغائب، و25 صفًا في الملف الأصغر تسلك ذلك المسار. وtvg-id="" تنطوي إلى لا شيء، لأن قاعدة الثقة تردّها. أما tvg-logo="" فلا تنطوي: تصل سلسلةً فارغة وتسافر حتى حقل الأيقونة في صف الكتالوج. 19 صفًا في أحد ملفَي العيّنة و42 في الآخر تحمل سمة شعار فارغة، فـ 61 صفًا في مجموعة عيّنات من 2,203 صفوف تحمل سلسلة فارغة حيث يحمل كل حقل آخر لا شيء. والشعار هو الشاذ لا لسبب سوى الترتيب الذي كُتبت به الطيّات الثلاث.

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

ما لم نقسه

بعض حدود هذا المحلِّل لا شاهد لها في عيّنتنا، وقول ذلك أنفع من التظاهر بأنها مختبَرة. لا مسار لقيمة بلا اقتباس: tvg-chno=18 بلا علامتَي اقتباس غير مرئية لنا. ولا سطر واحد من أسطر #EXTINF الـ 2,203 التي نملكها يستخدم قيمة بلا اقتباس، فلم يعضّنا ذلك قط، ولا نعرف كم هذا الشكل شائع في الواقع. والارتداد إلى logo المجردة فيه المشكلة نفسها معكوسة، إذ لا صف من صفوفنا يستخدمه، فالارتداد الذي نشحنه لا اختبار له أمام مُدخَل حقيقي.

كتابة هذه الصفحة كشفت خللًا كنا قد شحنّاه، وهذا أنفع ما فعلته. فعرض مُطابِق السمات عرضًا صريحًا يصلح للنشر جعل من البديهي أن النمط لا حدّ له من جهة البداية، فنمط logo كان يطابق داخل أي اسم سمة ينتهي بـ logo. ومن ذلك provider-logo، وهي سمة حقيقية توثّقها إضافة IPTV Simple لـ Kodi. فقائمة التشغيل التي تحمل provider-logo بلا tvg-logo كانت تأخذ الصورة الخطأ، ولم ينقذنا الخروج المبكر لأنه لا يعمل إلا حين تكون tvg-logo حاضرة. والثغرة نفسها جعلت tvg-id تطابق داخل x-tvg-id، وgroup-title وtvg-chno داخل أي شيء ينتهي بهما.

وفاجأنا أن العلاج البديهي خطأ. حدّ الكلمة لا يعمل هنا: كل اسم سمة في هذا الاصطلاح موصول بشَرطة، و- ليس محرف كلمة، فثمة حدّ قائم أصلًا بين الشرطة والاسم، و\btvg-logo ما زالت تطابق داخل x-tvg-logo. والشرط الذي تريده فعلًا هو أن يفتتح الاسم منطقة السمات أو أن يأتي بعد مسافة بيضاء. وفي .NET يكون ذلك نظرًا إلى الخلف (lookbehind) بالصيغة (?<=^|\s)، وهي محمولة على محركات أقل مما تتوقع: Python ترفض ترجمتها، لأن معظم المحركات تشترط نظرًا إلى الخلف بعرض ثابت، وهذا النظر يتناوب. والسمات الأربع كلها تحمل ذلك الحدّ الآن، والحالات الست أعلاه مثبَّتة بالاختبارات.

مشغّلنا ينفّذ هذا كله على جهازك، مقابل قائمة التشغيل التي تقدّمها أنت، وكل رفض يُنتج سلسلة تشخيص بدل استثناء، لأن ملفًا فيه 40 صفًا سيئًا من 3,000 عليه أن يستورد الـ 2,960 الباقية ويخبرك بما ضاع. وتطبيقا Windows وXbox ينفّذان ذلك العمل محليًا، وأنت توجّه أحدهما إلى مصدر تملكه أصلًا.

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

ما قياس هذه المادة26 ادعاءات، كل منها مزود بالأدلة التي تقف خلفه
المطالبةالأدلةعد
يعرّف RFC 8216 الوسم #EXTINF على أنه مدة، ثم فاصلة، ثم عنوان اختياري يمتد إلى نهاية السطر.RFC 8216 section 4.3.2.1المواصفاتلا ينطبق
يسمح RFC 8216 بمدة #EXTINF كسرية.RFC 8216 section 4.3.2.1المواصفاتلا ينطبق
يشترط RFC 8216 أن يكون #EXTM3U أول سطر في قائمة التشغيل.RFC 8216 section 4.3.1.1المواصفاتلا ينطبق
يقول RFC 8216 إن الوسوم تبدأ بـ #EXT وإنها حساسة لحالة الأحرف، فمطابقة البادئة مطابقةً ترتيبية هي السلوك المطابق للمواصفة.RFC 8216 section 4.1المواصفاتلا ينطبق
Extended M3U هو اصطلاح عصر Winamp الذي أدخل #EXTM3U و#EXTINF، ويستشهد به RFC 8216 بالاسم. وهو لا يحتوي أيًّا من رموز tvg-.RFC 8216 section 4.3.1.1, which reads: the EXTM3U tag indicates that the file is an Extended M3U [M3U] Playlist fileالمواصفاتلا ينطبق
M3U+ ليس اسم صيغة. m3u_plus وضع إخراج في خوادم المزوّدين التي تصل إليها ببيانات دخول المزوّد، وهو قيمة الاستعلام التي تحوّل التصدير من عناوين مجردة إلى صفوف تحمل tvg-id وtvg-logo وgroup-title.Provider login API, playlist export parameter type=m3u_plusالمواصفاتلا ينطبق
توثّق إضافة IPTV Simple لـ Kodi السمات catchup وcatchup-source وcatchup-days وtvg-shift وradio وprovider-logo وتوجيه #EXTGRP.kodi-pvr/pvr.iptvsimple README, playlist attribute referenceالمواصفاتلا ينطبق
aspect-ratio موثّقة لدى SS-IPTV، لا لدى Kodi.SS-IPTV playlist attribute documentationالمواصفاتلا ينطبق
tvg-language جاءت من iptv-org، التي أسقطتها منذ ذلك الحين مع tvg-country.iptv-org/iptv repository, playlist field historyالمواصفاتلا ينطبق
قائمة تشغيل بـ 50,000 مُدخَل على فضاء مفاتيح يقارب 2^31 تحمل احتمالًا يناهز 44 بالمئة لتصادم داخلي واحد على الأقل.Birthday bound, 1 - exp(-n(n-1)/2N) with N = 2^31 and n = 50,000, which evaluates to 0.4412المواصفاتلا ينطبق
كل سطر #EXTINF في مجموعة العيّنات يحمل مدة قدرها -1.n = 220322‏/08‏/2026
التقسيم عند أول فاصلة يكسر 30 مُدخَلًا من 308 في الملف الأصغر؛ و30 كلها تحمل قيمة user-agent فيها المقطع KHTML, like Gecko.n = 30822‏/08‏/2026
1,355 مُدخَلًا من 1,895 تحمل tvg-chno، موزّعة على 263 قيمة متمايزة، مع 107 صفوف تدّعي الرقم 1، ومجموعة واحدة تضم 7 قنوات كلها برقم 1.n = 189522‏/08‏/2026
في الملف الأكبر 92 مجموعة؛ 85 منها تحمل رقمًا واحدًا على الأقل، و33 من تلك الـ 85 فيها رقم مكرر.n = 189522‏/08‏/2026
56 من قيم tvg-chno البالغة 1,355 مرفوضة، وليس فيها قيمة رقمية واحدة: 49 رمز نظام بثّ، و4 عناصر نائبة هي المحرف # حرفيًا، و3 قيم نصية حرة. وسقف المحلِّل البالغ 99999 لم يعمل قط؛ وأكبر رقم في الملف هو 4999.n = 135522‏/08‏/2026
25 صفًا تستخدم ترقيم القنوات الفرعية على طريقة ATSC، موزّعة على 24 قيمة متمايزة تمتد من 22.1 إلى 36.2.n = 189522‏/08‏/2026
في الملف الأكبر، tvg-id غائبة عن 483 صفًا، وفارغة في 20 صفًا آخر، وحاضرة لكنها تفشل في قاعدة الشكل في 24 صفًا إضافيًا، فينتهي 527 صفًا (27.8 بالمئة) بلا معرّف صالح للاستخدام.n = 189522‏/08‏/2026
17 من حالات فشل الشكل الـ 24 مرفوضة لسبب واحد لا غير، هو أنها أقصر من ستة محارف، وهي تبدو معرّفات حقيقية: اسم قصير ثم نقطة ثم رمز بلد. أما الـ 7 الباقية فلا نقطة فيها.n = 2422‏/08‏/2026
في مجموعة عيّنات منفصلة من صادرات المزوّدين، كانت TS أشيع قيمة لـ tvg-id في مصدرين من ثلاثة، 446 صفًا و274 صفًا، وانهارت تلك القنوات الـ 720 التي لا صلة بينها في مفتاح قناة واحد. ومعرّف واحد اجتاز فحص الشكل كان مرتبطًا بـ 74 قناة لا صلة بينها.n = 720الجسم في وقت سابق
عناوين البث المكررة حقيقية ونادرة: 9 قيم عناوين مكررة تغطي 20 صفًا من 1,895 في الملف الأكبر، و0 من 308 في الأصغر.n = 220322‏/08‏/2026
6 صفوف في الملف الأكبر تشير إلى عنوان rtmp أو rtsp (5 و1)، وهي الصفوف الوحيدة في مجموعة العيّنات كلها التي يرفضها المحلِّل.n = 220322‏/08‏/2026
61 صفًا تحمل tvg-logo فارغة (19 في الملف الأصغر، و42 في الأكبر) والسلسلة الفارغة تنجو حتى صف الكتالوج، بينما تنطوي group-title الفارغة (25 صفًا) وtvg-id الفارغة كلتاهما إلى لا شيء.n = 220322‏/08‏/2026
tvg-name تظهر في كل صفوف الملف الأكبر البالغة 1,895، وtvg-country في 1,642 صفًا؛ والملف الأصغر يحمل 30 سمة user-agent، و2 سمة referrer، و32 سطر توجيه #EXTVLCOPT.n = 220322‏/08‏/2026
مجموعة العيّنات لا تحتوي أي قيمة سمة بلا اقتباس، ولا أي وسم #extinf بحروف صغيرة، ولا أي اسم سمة بحروف كبيرة، ولا أي مسافة حول علامة المساواة، ولا أي صف بعلامات اقتباس غير متوازنة، ولا أي سمة logo مجردة.n = 220322‏/08‏/2026
المقطع المبني يدويًا في هذه المقالة يعطي 4 صفوف محلَّلة، و3 صفوف كتالوج، و5 تشخيصات.n = 722‏/08‏/2026
للمحلِّل 9 اختبارات وحدة، منها اثنان يؤكدان أن الحقول لا تتسرب بين المُدخَلات. أما ماسح الفاصلة داخل الاقتباس، ورفض علامات الاقتباس غير المتوازنة، وحالة #EXTVLCOPT بين السطرين، والارتداد إلى logo المجردة، فلا اختبار لأي منها.n = 922‏/08‏/2026