Integrate native decoder TV and internet EPG artwork with Jellyfin 12.2

This commit is contained in:
2026-10-11 13:20:56 +02:00
parent d38c1595d8
commit a049a63d7a
21 changed files with 681 additions and 18 deletions
@@ -28,6 +28,7 @@ namespace Jellyfin.Plugin.Enigma2.Configuration
public string TranscodingPort { get; set; }
public bool EnableDebugLogging { get; set; }
public bool EnableVuRecordings { get; set; } = false;
public PluginConfiguration()
@@ -0,0 +1,54 @@
<!DOCTYPE html><html><head><title>Telewizja</title><meta name="viewport" content="width=device-width,initial-scale=1"><style>body{background:#181818;color:#eee;font:16px system-ui;margin:0} .content-primary{max-width:1200px;margin:auto;padding:24px}button,input,select{font:inherit;background:#303030;color:#eee;border:1px solid #666;border-radius:6px;padding:10px;margin:4px}button{cursor:pointer}button:hover{background:#455a64}a{color:#7cd5ef}img{object-fit:contain;border-radius:5px}section>div{flex:1;min-width:160px}</style></head><body>
<div id="decoderChannels" data-role="page" class="page type-interior pluginConfigurationPage">
<div data-role="content"><div class="content-primary">
<h1>Telewizja</h1><p>Cała lista kanałów dekodera. Ustawienia dotyczą Twojego konta.</p>
<input id="channelSearch" is="emby-input" placeholder="Szukaj kanału lub audycji" aria-label="Szukaj">
<select id="channelView" is="emby-select" aria-label="Widok"><option value="all">Wszystkie kanały</option><option value="favorites">Ulubione</option><option value="guide">Program telewizyjny</option><option value="hidden">Ukryte kanały</option></select>
<button id="saveDefault" is="emby-button">Ustaw ten widok jako domyślny</button>
<a id="nativeGuide" href="/web/index.html#/livetv">Natywny przewodnik, nagrania i timery</a>
<p id="channelMessage" role="status"></p><div id="channelRows"></div>
</div></div>
<script>
(function(){
const standalone=typeof ApiClient==='undefined';
const base=standalone?location.pathname.replace(/\/DecoderIntegration\/Channels\/?$/,''):'';
if(standalone){
let servers=[];try{servers=JSON.parse(localStorage.getItem('jellyfin_credentials')||'{}').Servers||[];}catch(e){}
const server=servers.find(s=>s.AccessToken&&s.UserId&&[s.ManualAddress,s.LocalAddress,s.RemoteAddress].some(a=>{try{return new URL(a).host===location.host;}catch(e){return false;}}));
if(!server){document.querySelector('#channelMessage').textContent='Zaloguj się w Jellyfinie z opcją „Zapamiętaj mnie”, a następnie otwórz tę stronę ponownie.';const link=document.createElement('a');link.href=base+'/web/index.html#/login';link.textContent='Przejdź do logowania Jellyfin';document.querySelector('#channelRows').append(link);return;}
window.ApiClient={getCurrentUserId:()=>server.UserId,serverId:()=>server.Id,getUrl:(path,query)=>base+'/'+path+(query?'?'+new URLSearchParams(query):''),
ajax:async options=>{const response=await fetch(options.url,{method:options.type||'GET',headers:{'Authorization':'MediaBrowser Token="'+server.AccessToken+'", Client="DecoderIntegration", Device="Browser", DeviceId="decoder-integration", Version="7"','Content-Type':'application/json'},body:options.data});if(!response.ok)throw Error('HTTP '+response.status);return options.dataType==='json'?response.json():undefined;},
getImageUrl:(id,options)=>base+'/Items/'+encodeURIComponent(id)+'/Images/'+options.type+'?'+new URLSearchParams({tag:options.tag,maxWidth:options.maxWidth})};
}
const page=document.querySelector('#decoderChannels'); let channels=[], preferences, programs={}, saving=Promise.resolve();
const q=id=>page.querySelector('#'+id), key=id=>id.replace(/-/g,'').toLowerCase();
function request(path,type='GET',data){return ApiClient.ajax({type,url:ApiClient.getUrl(path),data:data?JSON.stringify(data):undefined,contentType:'application/json',dataType:type==='GET'?'json':undefined});}
function save(){const snapshot=JSON.parse(JSON.stringify(preferences));saving=saving.catch(()=>{}).then(()=>request('DecoderIntegration/Preferences','PUT',snapshot));saving.catch(()=>q('channelMessage').textContent='Nie udało się zapisać ustawień.');}
function button(text,action){const b=document.createElement('button');b.textContent=text;b.className='raised emby-button';b.onclick=action;return b;}
function details(item){location.href=base+'/web/index.html#/details?id='+encodeURIComponent(item.Id)+'&serverId='+encodeURIComponent(ApiClient.serverId());}
q('nativeGuide').href=base+'/web/index.html#/livetv';
function toggle(field,id){const a=preferences[field],i=a.indexOf(id);if(i<0)a.push(id);else a.splice(i,1);if(field==='FavoriteIds')request('Users/'+ApiClient.getCurrentUserId()+'/FavoriteItems/'+id,i<0?'POST':'DELETE').catch(()=>q('channelMessage').textContent='Nie udało się zsynchronizować ulubionych z Jellyfinem.');save();render();}
function render(){
const view=q('channelView').value,search=q('channelSearch').value.toLocaleLowerCase(),hidden=new Set(preferences.HiddenIds),fav=new Set(preferences.FavoriteIds);
const order=new Map(preferences.OrderedIds.map((id,i)=>[id,i]));
const sorted=[...channels].sort((a,b)=>(order.get(key(a.Id))??99999)-(order.get(key(b.Id))??99999));
const visible=sorted.filter(c=>{const id=key(c.Id);return (view==='hidden'?hidden.has(id):!hidden.has(id))&&(view!=='favorites'||fav.has(id))&&(c.Name+' '+(c.CurrentProgram?.Name||'')).toLocaleLowerCase().includes(search);});
q('channelRows').replaceChildren();q('channelMessage').textContent=visible.length+' / '+channels.length+' kanałów';
visible.forEach(c=>{const id=key(c.Id),row=document.createElement('section');row.style.cssText='padding:1em 0;border-bottom:1px solid #666;display:flex;gap:1em;flex-wrap:wrap;align-items:center';
if(c.ImageTags?.Primary){const image=document.createElement('img');image.src=ApiClient.getImageUrl(c.Id,{type:'Primary',tag:c.ImageTags.Primary,maxWidth:100});image.alt='';image.width=80;image.loading='lazy';row.append(image);}
row.append(button((c.Number?c.Number+'. ':'')+c.Name,()=>details(c)),button(fav.has(id)?'★ Ulubiony':'☆ Ulubiony',()=>toggle('FavoriteIds',id)),button(hidden.has(id)?'Pokaż':'Ukryj',()=>toggle('HiddenIds',id)));
row.append(button('↑',()=>{const list=sorted.map(x=>key(x.Id)),i=list.indexOf(id);if(i>0){[list[i-1],list[i]]=[list[i],list[i-1]];preferences.OrderedIds=list;save();render();}}));
const events=view==='guide'?(programs[c.Id]||[]):[c.CurrentProgram].filter(Boolean);
events.forEach(p=>{const card=document.createElement('div');card.style.cssText='max-width:22em;cursor:pointer';card.onclick=()=>details(p);
if(p.ImageTags?.Primary){const image=document.createElement('img');image.src=ApiClient.getImageUrl(p.Id,{type:'Primary',tag:p.ImageTags.Primary,maxWidth:240});image.width=160;image.alt='';image.loading='lazy';card.append(image);}
const text=document.createElement('p');text.textContent=new Date(p.StartDate).toLocaleTimeString([],{hour:'2-digit',minute:'2-digit'})+'–'+new Date(p.EndDate).toLocaleTimeString([],{hour:'2-digit',minute:'2-digit'})+' '+p.Name;card.append(text);row.append(card);
});q('channelRows').append(row);
});
}
async function guide(){if(Object.keys(programs).length)return;const now=new Date(),until=new Date(now.getTime()+6*3600000);let offset=0;while(true){const result=await ApiClient.ajax({type:'GET',url:ApiClient.getUrl('LiveTv/Programs',{UserId:ApiClient.getCurrentUserId(),MinEndDate:now.toISOString(),MaxStartDate:until.toISOString(),StartIndex:offset,Limit:500}),dataType:'json'});for(const p of result.Items)(programs[p.ChannelId]??=[]).push(p);render();offset+=result.Items.length;if(!result.Items.length||offset>=result.TotalRecordCount)break;}render();}
q('channelSearch').oninput=()=>preferences&&render();q('channelView').onchange=()=>{render();if(q('channelView').value==='guide')guide().catch(()=>q('channelMessage').textContent='Nie udało się pobrać przewodnika.');};
q('saveDefault').onclick=()=>{if(q('channelView').value!=='hidden'){preferences.DefaultView=q('channelView').value;save();}};
async function load(){try{preferences=await request('DecoderIntegration/Preferences');channels=[];programs={};let offset=0;while(true){const result=await ApiClient.ajax({type:'GET',url:ApiClient.getUrl('LiveTv/Channels',{UserId:ApiClient.getCurrentUserId(),AddCurrentProgram:true,EnableUserData:true,DecoderIncludeHidden:true,StartIndex:offset,Limit:250}),dataType:'json'});channels.push(...result.Items);offset+=result.Items.length;if(!result.Items.length||offset>=result.TotalRecordCount)break;}preferences.FavoriteIds=[...new Set([...preferences.FavoriteIds,...channels.filter(c=>c.UserData?.IsFavorite).map(c=>key(c.Id))])];q('channelView').value=preferences.DefaultView;render();if(preferences.DefaultView==='guide')await guide();}catch(e){q('channelMessage').textContent='Nie udało się pobrać kanałów. Sprawdź dostęp do telewizji na swoim koncie.';}}
page.addEventListener('pageshow',load);if(standalone)load();
})();
</script></div></body></html>
@@ -143,6 +143,11 @@
</div>
</div>
<label class="checkboxContainer">
<input is="emby-checkbox" type="checkbox" id="chkEnableVuRecordings" />
<span>Obsługuj nagrania z Vu+ (Enigma2 Recordings)</span>
</label>
<div class="fieldDescription">Włącza bibliotekę nagrań dekodera. Zmiana wymaga ponownego uruchomienia Jellyfina. Wyłączenie nie usuwa plików nagrań.</div>
<label class="checkboxContainer">
<input is="emby-checkbox" type="checkbox" id="chkDebugLogging" />
<span>Enable Enigma2 debug logging</span>
@@ -182,6 +187,7 @@
document.getElementById('chkTranscodedStream').checked = config.TranscodedStream || false;
document.getElementById('txtTranscodingPort').value = config.TranscodingPort || "";
document.getElementById('chkDebugLogging').checked = config.EnableDebugLogging || false;
document.getElementById('chkEnableVuRecordings').checked = config.EnableVuRecordings || false;
Dashboard.hideLoadingMsg();
});
@@ -208,6 +214,7 @@
config.TranscodedStream = document.getElementById('chkTranscodedStream').checked;
config.TranscodingPort = document.getElementById('txtTranscodingPort').value;
config.EnableDebugLogging = document.getElementById('chkDebugLogging').checked;
config.EnableVuRecordings = document.getElementById('chkEnableVuRecordings').checked;
ApiClient.updatePluginConfiguration(Enigma2ConfigurationPage.pluginUniqueId, config).then(Dashboard.processPluginConfigurationUpdateResult);
});