@whoisjayd/tizenliv 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +674 -0
- package/README.md +88 -0
- package/dist/userScript.js +312 -0
- package/launcher.html +82 -0
- package/package.json +36 -0
- package/src/adblocker.js +20 -0
- package/src/consent.js +15 -0
- package/src/dom.js +45 -0
- package/src/network.js +83 -0
- package/src/rules.js +62 -0
- package/src/scripts.js +61 -0
- package/src/userScript.js +3 -0
package/README.md
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# TizenLIV
|
|
2
|
+
|
|
3
|
+
TizenLIV is a standalone TizenBrew site-mod module for SonyLIV ad blocking.
|
|
4
|
+
|
|
5
|
+
This project is separate from TizenTube. TizenTube was used only as a reference for the TizenBrew module shape and build target.
|
|
6
|
+
|
|
7
|
+
## What it does
|
|
8
|
+
|
|
9
|
+
- Blocks known SonyLIV telemetry/ad beacon requests.
|
|
10
|
+
- Opens through a packaged launcher page that tries to set a desktop Chrome user agent before SonyLIV's first page request.
|
|
11
|
+
- Strips the client-side Google IMA bootstrap script that public filter research associates with SonyLIV ads.
|
|
12
|
+
- Presets SonyLIV cookie consent localStorage keys to reduce consent/adblock popups.
|
|
13
|
+
- Hides/removes simple adblock and ad banner overlays.
|
|
14
|
+
|
|
15
|
+
## What it does not do
|
|
16
|
+
|
|
17
|
+
- It does not bypass DRM, subscriptions, login, geography checks, or paid access.
|
|
18
|
+
- It does not attempt to decrypt or rewrite protected media streams.
|
|
19
|
+
- It does not include SponsorBlock or YouTube-specific features.
|
|
20
|
+
|
|
21
|
+
## Research notes
|
|
22
|
+
|
|
23
|
+
The initial rules are intentionally conservative because SonyLIV playback can break if ad scripts are blocked too aggressively:
|
|
24
|
+
|
|
25
|
+
- TizenBrew site modification modules require `packageType: "mods"`, `appName`, `websiteURL`, and `main` in `package.json`.
|
|
26
|
+
- AdGuard public rules note that fully blocking `imasdk.googleapis.com/js/sdkloader/ima3.js` can break SonyLIV in some app contexts.
|
|
27
|
+
- AdGuard public rules allow `ima3_dai.js` and SonyLIV player ad manager files to avoid player breakage.
|
|
28
|
+
- AdGuard public rules set SonyLIV cookie consent localStorage keys.
|
|
29
|
+
- AdGuard public rules identify `api-godavari.sonyliv.com/beacon` as tracking/telemetry.
|
|
30
|
+
- TizenBrew serves package files through `http://127.0.0.1:8081/module/<package>/...`, so this module starts at `launcher.html`, calls `tizen.websetting.setUserAgentString(...)`, then redirects to SonyLIV.
|
|
31
|
+
|
|
32
|
+
## Installation in TizenBrew
|
|
33
|
+
|
|
34
|
+
### From npm
|
|
35
|
+
|
|
36
|
+
1. Open TizenBrew on your Samsung TV.
|
|
37
|
+
2. Open the module manager.
|
|
38
|
+
3. Add this npm module:
|
|
39
|
+
|
|
40
|
+
```text
|
|
41
|
+
@whoisjayd/tizenliv
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
4. Save/apply the module list.
|
|
45
|
+
5. Restart TizenBrew if it does not refresh automatically.
|
|
46
|
+
6. Launch **TizenLIV** from TizenBrew.
|
|
47
|
+
|
|
48
|
+
TizenLIV now opens a small local launcher first. It should immediately redirect to SonyLIV after trying to apply a desktop Chrome user agent. This avoids requiring you to manually change the global TizenBrew user-agent setting.
|
|
49
|
+
|
|
50
|
+
### From source
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
git clone https://github.com/whoisjayd/tizenliv.git
|
|
54
|
+
cd tizenliv
|
|
55
|
+
npm install
|
|
56
|
+
npm run build
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
The TizenBrew script entrypoint is `dist/userScript.js`. The launch entrypoint is `launcher.html`, which redirects to SonyLIV after applying the user agent.
|
|
60
|
+
|
|
61
|
+
## Development
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
npm install
|
|
65
|
+
npm test
|
|
66
|
+
npm run build
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## Package metadata
|
|
70
|
+
|
|
71
|
+
```json
|
|
72
|
+
{
|
|
73
|
+
"name": "@whoisjayd/tizenliv",
|
|
74
|
+
"appName": "TizenLIV",
|
|
75
|
+
"packageType": "mods",
|
|
76
|
+
"websiteURL": "http://127.0.0.1:8081/module/%40whoisjayd%2Ftizenliv/launcher.html",
|
|
77
|
+
"main": "dist/userScript.js",
|
|
78
|
+
"evaluateScriptOnDocumentStart": true
|
|
79
|
+
}
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## Troubleshooting
|
|
83
|
+
|
|
84
|
+
- If the page still times out, the TV may be unable to reach SonyLIV from TizenBrew's WebView because of network, region, TLS, or platform blocking. The launcher can spoof the user agent, but it cannot fix a domain that the TV/browser cannot connect to.
|
|
85
|
+
- If the launcher page stays visible, Tizen WebView may have denied `tizen.websetting`; wait a moment for fallback redirect or restart TizenBrew.
|
|
86
|
+
- If playback freezes or stays stuck at loading, SonyLIV may require one of its ad manager scripts for player initialization.
|
|
87
|
+
- Keep `ima3_dai.js` and `player.sonyliv.com/.../spnadmanager.js` allowed unless manual testing proves they are safe to strip.
|
|
88
|
+
- If ads return, compare the current network calls with uBlock Origin/Brave on desktop and add narrow host/path rules only.
|
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
(function () {
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
function _arrayLikeToArray(r, a) {
|
|
5
|
+
(null == a || a > r.length) && (a = r.length);
|
|
6
|
+
for (var e = 0, n = Array(a); e < a; e++) n[e] = r[e];
|
|
7
|
+
return n;
|
|
8
|
+
}
|
|
9
|
+
function _createForOfIteratorHelper(r, e) {
|
|
10
|
+
var t = "undefined" != typeof Symbol && r[Symbol.iterator] || r["@@iterator"];
|
|
11
|
+
if (!t) {
|
|
12
|
+
if (Array.isArray(r) || (t = _unsupportedIterableToArray(r)) || e) {
|
|
13
|
+
t && (r = t);
|
|
14
|
+
var n = 0,
|
|
15
|
+
F = function () {};
|
|
16
|
+
return {
|
|
17
|
+
s: F,
|
|
18
|
+
n: function () {
|
|
19
|
+
return n >= r.length ? {
|
|
20
|
+
done: true
|
|
21
|
+
} : {
|
|
22
|
+
done: false,
|
|
23
|
+
value: r[n++]
|
|
24
|
+
};
|
|
25
|
+
},
|
|
26
|
+
e: function (r) {
|
|
27
|
+
throw r;
|
|
28
|
+
},
|
|
29
|
+
f: F
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.");
|
|
33
|
+
}
|
|
34
|
+
var o,
|
|
35
|
+
a = true,
|
|
36
|
+
u = false;
|
|
37
|
+
return {
|
|
38
|
+
s: function () {
|
|
39
|
+
t = t.call(r);
|
|
40
|
+
},
|
|
41
|
+
n: function () {
|
|
42
|
+
var r = t.next();
|
|
43
|
+
return a = r.done, r;
|
|
44
|
+
},
|
|
45
|
+
e: function (r) {
|
|
46
|
+
u = true, o = r;
|
|
47
|
+
},
|
|
48
|
+
f: function () {
|
|
49
|
+
try {
|
|
50
|
+
a || null == t.return || t.return();
|
|
51
|
+
} finally {
|
|
52
|
+
if (u) throw o;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
function _typeof(o) {
|
|
58
|
+
"@babel/helpers - typeof";
|
|
59
|
+
|
|
60
|
+
return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (o) {
|
|
61
|
+
return typeof o;
|
|
62
|
+
} : function (o) {
|
|
63
|
+
return o && "function" == typeof Symbol && o.constructor === Symbol && o !== Symbol.prototype ? "symbol" : typeof o;
|
|
64
|
+
}, _typeof(o);
|
|
65
|
+
}
|
|
66
|
+
function _unsupportedIterableToArray(r, a) {
|
|
67
|
+
if (r) {
|
|
68
|
+
if ("string" == typeof r) return _arrayLikeToArray(r, a);
|
|
69
|
+
var t = {}.toString.call(r).slice(8, -1);
|
|
70
|
+
return "Object" === t && r.constructor && (t = r.constructor.name), "Map" === t || "Set" === t ? Array.from(r) : "Arguments" === t || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(t) ? _arrayLikeToArray(r, a) : void 0;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
var SONYLIV_CSS_SELECTORS = ['[class*="adblock"]', '[id*="adblock"]', '[class*="AdBlock"]', '[id*="AdBlock"]', '[class*="ad-banner"]', '[id*="ad-banner"]', '[class*="AdBanner"]', '[id*="AdBanner"]'];
|
|
75
|
+
var BLOCK_RULES = [{
|
|
76
|
+
reason: 'SonyLIV beacon endpoint',
|
|
77
|
+
matches: function matches(url) {
|
|
78
|
+
return url.hostname === 'api-godavari.sonyliv.com' && /\/beacon(?:\?|\/|$)/i.test(url.pathname + url.search);
|
|
79
|
+
}
|
|
80
|
+
}];
|
|
81
|
+
var CONSENT_STORAGE_ENTRIES = [['cookie_acceptance', 'true'], ['essential_cookies_acceptance', 'true'], ['session_cookies_acceptance', 'true'], ['analytics_cookies_acceptance', 'false']];
|
|
82
|
+
function getBlockedReason(input) {
|
|
83
|
+
var url = toUrl(input);
|
|
84
|
+
if (!url) return null;
|
|
85
|
+
var _iterator = _createForOfIteratorHelper(BLOCK_RULES),
|
|
86
|
+
_step;
|
|
87
|
+
try {
|
|
88
|
+
for (_iterator.s(); !(_step = _iterator.n()).done;) {
|
|
89
|
+
var rule = _step.value;
|
|
90
|
+
if (rule.matches(url)) return rule.reason;
|
|
91
|
+
}
|
|
92
|
+
} catch (err) {
|
|
93
|
+
_iterator.e(err);
|
|
94
|
+
} finally {
|
|
95
|
+
_iterator.f();
|
|
96
|
+
}
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
function shouldStripScriptSource(input) {
|
|
100
|
+
var url = toUrl(input);
|
|
101
|
+
if (!url) return false;
|
|
102
|
+
return url.hostname === 'imasdk.googleapis.com' && url.pathname === '/js/sdkloader/ima3.js';
|
|
103
|
+
}
|
|
104
|
+
function getConsentStorageEntries() {
|
|
105
|
+
return CONSENT_STORAGE_ENTRIES.map(function cloneEntry(entry) {
|
|
106
|
+
return [entry[0], entry[1]];
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
function toUrl(input) {
|
|
110
|
+
try {
|
|
111
|
+
var base = typeof window !== 'undefined' && window.location ? window.location.href : 'https://www.sonyliv.com/';
|
|
112
|
+
if (input && _typeof(input) === 'object' && 'url' in input) {
|
|
113
|
+
return new URL(input.url, base);
|
|
114
|
+
}
|
|
115
|
+
return new URL(String(input), base);
|
|
116
|
+
} catch (error) {
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function applyConsentStorage(loggerName) {
|
|
122
|
+
if (typeof window === 'undefined' || !window.localStorage) return;
|
|
123
|
+
getConsentStorageEntries().forEach(function setEntry(entry) {
|
|
124
|
+
try {
|
|
125
|
+
window.localStorage.setItem(entry[0], entry[1]);
|
|
126
|
+
} catch (error) {
|
|
127
|
+
if (typeof console !== 'undefined' && console.warn) {
|
|
128
|
+
console.warn('[' + loggerName + '] could not set localStorage key ' + entry[0], error);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function installDomCleaner(selectors, loggerName) {
|
|
135
|
+
if (typeof document === 'undefined' || !Array.isArray(selectors) || selectors.length === 0) return;
|
|
136
|
+
injectStyle(selectors);
|
|
137
|
+
removeMatches(selectors, loggerName);
|
|
138
|
+
if (typeof MutationObserver === 'function' && document.documentElement) {
|
|
139
|
+
var observer = new MutationObserver(function onMutations() {
|
|
140
|
+
removeMatches(selectors, loggerName);
|
|
141
|
+
});
|
|
142
|
+
observer.observe(document.documentElement, {
|
|
143
|
+
childList: true,
|
|
144
|
+
subtree: true
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
function injectStyle(selectors) {
|
|
149
|
+
var style = document.createElement('style');
|
|
150
|
+
style.setAttribute('data-tizen-adblock', 'true');
|
|
151
|
+
style.textContent = selectors.join(',\n') + ' { display: none !important; visibility: hidden !important; }';
|
|
152
|
+
var target = document.head || document.documentElement;
|
|
153
|
+
if (target) target.appendChild(style);
|
|
154
|
+
}
|
|
155
|
+
function removeMatches(selectors, loggerName) {
|
|
156
|
+
selectors.forEach(function removeSelector(selector) {
|
|
157
|
+
try {
|
|
158
|
+
var nodes = document.querySelectorAll(selector);
|
|
159
|
+
for (var index = 0; index < nodes.length; index += 1) {
|
|
160
|
+
var node = nodes[index];
|
|
161
|
+
if (node && node.parentNode) {
|
|
162
|
+
node.parentNode.removeChild(node);
|
|
163
|
+
if (typeof console !== 'undefined' && console.debug) {
|
|
164
|
+
console.debug('[' + loggerName + '] removed DOM ad selector ' + selector);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
} catch (error) {
|
|
169
|
+
// Ignore selectors unsupported by older Tizen Chromium builds.
|
|
170
|
+
}
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function installNetworkBlocker(getBlockedReason, loggerName) {
|
|
175
|
+
patchFetch(getBlockedReason, loggerName);
|
|
176
|
+
patchXMLHttpRequest(getBlockedReason, loggerName);
|
|
177
|
+
}
|
|
178
|
+
function patchFetch(getBlockedReason, loggerName) {
|
|
179
|
+
if (typeof window === 'undefined' || typeof window.fetch !== 'function' || window.fetch.__tizenAdBlockPatched) {
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
var originalFetch = window.fetch;
|
|
183
|
+
function blockedFetch(input, init) {
|
|
184
|
+
var url = getRequestUrl(input);
|
|
185
|
+
var reason = getBlockedReason(url);
|
|
186
|
+
if (reason) {
|
|
187
|
+
logBlock(loggerName, url, reason);
|
|
188
|
+
return Promise.resolve(createEmptyResponse());
|
|
189
|
+
}
|
|
190
|
+
return originalFetch.apply(this, arguments);
|
|
191
|
+
}
|
|
192
|
+
blockedFetch.__tizenAdBlockPatched = true;
|
|
193
|
+
blockedFetch.__tizenAdBlockOriginal = originalFetch;
|
|
194
|
+
window.fetch = blockedFetch;
|
|
195
|
+
}
|
|
196
|
+
function patchXMLHttpRequest(getBlockedReason, loggerName) {
|
|
197
|
+
if (typeof window === 'undefined' || typeof window.XMLHttpRequest !== 'function') return;
|
|
198
|
+
var proto = window.XMLHttpRequest.prototype;
|
|
199
|
+
if (proto.open.__tizenAdBlockPatched) return;
|
|
200
|
+
var originalOpen = proto.open;
|
|
201
|
+
proto.open = function patchedOpen(method, url) {
|
|
202
|
+
var requestUrl = getRequestUrl(url);
|
|
203
|
+
var reason = getBlockedReason(requestUrl);
|
|
204
|
+
if (reason) {
|
|
205
|
+
this.__tizenAdBlockBlocked = true;
|
|
206
|
+
this.__tizenAdBlockOriginalUrl = requestUrl;
|
|
207
|
+
logBlock(loggerName, requestUrl, reason);
|
|
208
|
+
var args = Array.prototype.slice.call(arguments);
|
|
209
|
+
args[1] = 'data:text/plain,';
|
|
210
|
+
return originalOpen.apply(this, args);
|
|
211
|
+
}
|
|
212
|
+
return originalOpen.apply(this, arguments);
|
|
213
|
+
};
|
|
214
|
+
proto.open.__tizenAdBlockPatched = true;
|
|
215
|
+
}
|
|
216
|
+
function getRequestUrl(input) {
|
|
217
|
+
if (input && _typeof(input) === 'object' && typeof input.url === 'string') return input.url;
|
|
218
|
+
return String(input || '');
|
|
219
|
+
}
|
|
220
|
+
function createEmptyResponse() {
|
|
221
|
+
if (typeof Response === 'function') {
|
|
222
|
+
return new Response('', {
|
|
223
|
+
status: 204,
|
|
224
|
+
statusText: 'No Content',
|
|
225
|
+
headers: {
|
|
226
|
+
'Content-Type': 'text/plain'
|
|
227
|
+
}
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
return {
|
|
231
|
+
ok: true,
|
|
232
|
+
status: 204,
|
|
233
|
+
statusText: 'No Content',
|
|
234
|
+
text: function text() {
|
|
235
|
+
return Promise.resolve('');
|
|
236
|
+
},
|
|
237
|
+
json: function json() {
|
|
238
|
+
return Promise.resolve({});
|
|
239
|
+
},
|
|
240
|
+
blob: function blob() {
|
|
241
|
+
return Promise.resolve(new Blob(['']));
|
|
242
|
+
}
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
function logBlock(loggerName, url, reason) {
|
|
246
|
+
if (typeof console !== 'undefined' && console.debug) {
|
|
247
|
+
console.debug('[' + loggerName + '] blocked ' + url + ' (' + reason + ')');
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function installScriptSourceStripper(shouldStripScriptSource, loggerName) {
|
|
252
|
+
if (typeof document === 'undefined' || !document.createElement || document.createElement.__tizenAdBlockPatched) {
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
var originalCreateElement = document.createElement.bind(document);
|
|
256
|
+
document.createElement = function patchedCreateElement(tagName, options) {
|
|
257
|
+
var element = originalCreateElement(tagName, options);
|
|
258
|
+
if (String(tagName).toLowerCase() === 'script') {
|
|
259
|
+
patchScriptElement(element, shouldStripScriptSource, loggerName);
|
|
260
|
+
}
|
|
261
|
+
return element;
|
|
262
|
+
};
|
|
263
|
+
document.createElement.__tizenAdBlockPatched = true;
|
|
264
|
+
}
|
|
265
|
+
function patchScriptElement(element, shouldStripScriptSource, loggerName) {
|
|
266
|
+
var descriptor = getScriptSrcDescriptor();
|
|
267
|
+
var originalSetAttribute = element.setAttribute;
|
|
268
|
+
if (descriptor && descriptor.configurable) {
|
|
269
|
+
Object.defineProperty(element, 'src', {
|
|
270
|
+
configurable: true,
|
|
271
|
+
enumerable: descriptor.enumerable,
|
|
272
|
+
get: function getSrc() {
|
|
273
|
+
return descriptor.get.call(this);
|
|
274
|
+
},
|
|
275
|
+
set: function setSrc(value) {
|
|
276
|
+
descriptor.set.call(this, getSafeScriptSource(value, shouldStripScriptSource, loggerName));
|
|
277
|
+
}
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
element.setAttribute = function patchedSetAttribute(name, value) {
|
|
281
|
+
if (String(name).toLowerCase() === 'src') {
|
|
282
|
+
return originalSetAttribute.call(this, name, getSafeScriptSource(value, shouldStripScriptSource, loggerName));
|
|
283
|
+
}
|
|
284
|
+
return originalSetAttribute.apply(this, arguments);
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
function getScriptSrcDescriptor() {
|
|
288
|
+
if (typeof HTMLScriptElement === 'undefined') return null;
|
|
289
|
+
return Object.getOwnPropertyDescriptor(HTMLScriptElement.prototype, 'src');
|
|
290
|
+
}
|
|
291
|
+
function getSafeScriptSource(value, shouldStripScriptSource, loggerName) {
|
|
292
|
+
var source = String(value || '');
|
|
293
|
+
if (!shouldStripScriptSource(source)) return value;
|
|
294
|
+
if (typeof console !== 'undefined' && console.debug) {
|
|
295
|
+
console.debug('[' + loggerName + '] stripped script source ' + source);
|
|
296
|
+
}
|
|
297
|
+
return 'data:text/javascript,/* stripped by ' + encodeURIComponent(loggerName) + ' */';
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function installSonyLivAdBlocker() {
|
|
301
|
+
applyConsentStorage('TizenLIV');
|
|
302
|
+
installNetworkBlocker(getBlockedReason, 'TizenLIV');
|
|
303
|
+
installScriptSourceStripper(shouldStripScriptSource, 'TizenLIV');
|
|
304
|
+
installDomCleaner(SONYLIV_CSS_SELECTORS, 'TizenLIV');
|
|
305
|
+
if (typeof console !== 'undefined' && console.info) {
|
|
306
|
+
console.info('[TizenLIV] SonyLIV adblock rules installed');
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
installSonyLivAdBlocker();
|
|
311
|
+
|
|
312
|
+
})();
|
package/launcher.html
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
+
<title>Launching SonyLIV…</title>
|
|
7
|
+
<style>
|
|
8
|
+
html,
|
|
9
|
+
body {
|
|
10
|
+
height: 100%;
|
|
11
|
+
margin: 0;
|
|
12
|
+
background: #050505;
|
|
13
|
+
color: #f5f5f5;
|
|
14
|
+
font-family: Arial, Helvetica, sans-serif;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
body {
|
|
18
|
+
align-items: center;
|
|
19
|
+
display: flex;
|
|
20
|
+
justify-content: center;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
main {
|
|
24
|
+
max-width: 720px;
|
|
25
|
+
padding: 48px;
|
|
26
|
+
text-align: center;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
h1 {
|
|
30
|
+
font-size: 32px;
|
|
31
|
+
font-weight: 700;
|
|
32
|
+
margin: 0 0 16px;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
p {
|
|
36
|
+
color: #b8b8b8;
|
|
37
|
+
font-size: 18px;
|
|
38
|
+
line-height: 1.5;
|
|
39
|
+
margin: 0;
|
|
40
|
+
}
|
|
41
|
+
</style>
|
|
42
|
+
</head>
|
|
43
|
+
<body>
|
|
44
|
+
<main>
|
|
45
|
+
<h1>Launching SonyLIV</h1>
|
|
46
|
+
<p>Applying the TizenLIV browser profile, then opening SonyLIV automatically.</p>
|
|
47
|
+
</main>
|
|
48
|
+
|
|
49
|
+
<script>
|
|
50
|
+
(function () {
|
|
51
|
+
var SONYLIV_URL = 'https://www.sonyliv.com/';
|
|
52
|
+
var DESKTOP_USER_AGENT = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36';
|
|
53
|
+
var redirected = false;
|
|
54
|
+
|
|
55
|
+
function goToSonyLiv() {
|
|
56
|
+
if (redirected) {
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
redirected = true;
|
|
61
|
+
window.location.replace(SONYLIV_URL);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function setSonyLivUserAgent() {
|
|
65
|
+
try {
|
|
66
|
+
if (window.tizen && tizen.websetting && typeof tizen.websetting.setUserAgentString === 'function') {
|
|
67
|
+
tizen.websetting.setUserAgentString(DESKTOP_USER_AGENT, goToSonyLiv, goToSonyLiv);
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
} catch (error) {
|
|
71
|
+
// Fall back to direct navigation if this Tizen profile blocks websetting access.
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
goToSonyLiv();
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
setSonyLivUserAgent();
|
|
78
|
+
window.setTimeout(goToSonyLiv, 1500);
|
|
79
|
+
}());
|
|
80
|
+
</script>
|
|
81
|
+
</body>
|
|
82
|
+
</html>
|
package/package.json
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@whoisjayd/tizenliv",
|
|
3
|
+
"appName": "TizenLIV",
|
|
4
|
+
"version": "0.1.1",
|
|
5
|
+
"description": "SonyLIV ad blocking module for TizenBrew.",
|
|
6
|
+
"packageType": "mods",
|
|
7
|
+
"websiteURL": "http://127.0.0.1:8081/module/%40whoisjayd%2Ftizenliv/launcher.html",
|
|
8
|
+
"main": "dist/userScript.js",
|
|
9
|
+
"evaluateScriptOnDocumentStart": true,
|
|
10
|
+
"type": "module",
|
|
11
|
+
"author": "whoisjayd",
|
|
12
|
+
"license": "GPL-3.0-only",
|
|
13
|
+
"repository": {
|
|
14
|
+
"type": "git",
|
|
15
|
+
"url": "git+https://github.com/whoisjayd/tizenliv.git"
|
|
16
|
+
},
|
|
17
|
+
"files": [
|
|
18
|
+
"dist",
|
|
19
|
+
"src",
|
|
20
|
+
"launcher.html",
|
|
21
|
+
"README.md",
|
|
22
|
+
"LICENSE"
|
|
23
|
+
],
|
|
24
|
+
"scripts": {
|
|
25
|
+
"build": "rollup -c",
|
|
26
|
+
"test": "vitest run",
|
|
27
|
+
"prepack": "npm run build"
|
|
28
|
+
},
|
|
29
|
+
"devDependencies": {
|
|
30
|
+
"@babel/preset-env": "^7.22.9",
|
|
31
|
+
"@rollup/plugin-babel": "^6.0.4",
|
|
32
|
+
"@rollup/plugin-node-resolve": "^15.2.3",
|
|
33
|
+
"rollup": "^4.18.1",
|
|
34
|
+
"vitest": "^1.6.0"
|
|
35
|
+
}
|
|
36
|
+
}
|
package/src/adblocker.js
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { applyConsentStorage } from './consent.js';
|
|
2
|
+
import { installDomCleaner } from './dom.js';
|
|
3
|
+
import { installNetworkBlocker } from './network.js';
|
|
4
|
+
import {
|
|
5
|
+
SONYLIV_CSS_SELECTORS,
|
|
6
|
+
getBlockedReason,
|
|
7
|
+
shouldStripScriptSource,
|
|
8
|
+
} from './rules.js';
|
|
9
|
+
import { installScriptSourceStripper } from './scripts.js';
|
|
10
|
+
|
|
11
|
+
export function installSonyLivAdBlocker() {
|
|
12
|
+
applyConsentStorage('TizenLIV');
|
|
13
|
+
installNetworkBlocker(getBlockedReason, 'TizenLIV');
|
|
14
|
+
installScriptSourceStripper(shouldStripScriptSource, 'TizenLIV');
|
|
15
|
+
installDomCleaner(SONYLIV_CSS_SELECTORS, 'TizenLIV');
|
|
16
|
+
|
|
17
|
+
if (typeof console !== 'undefined' && console.info) {
|
|
18
|
+
console.info('[TizenLIV] SonyLIV adblock rules installed');
|
|
19
|
+
}
|
|
20
|
+
}
|
package/src/consent.js
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { getConsentStorageEntries } from './rules.js';
|
|
2
|
+
|
|
3
|
+
export function applyConsentStorage(loggerName) {
|
|
4
|
+
if (typeof window === 'undefined' || !window.localStorage) return;
|
|
5
|
+
|
|
6
|
+
getConsentStorageEntries().forEach(function setEntry(entry) {
|
|
7
|
+
try {
|
|
8
|
+
window.localStorage.setItem(entry[0], entry[1]);
|
|
9
|
+
} catch (error) {
|
|
10
|
+
if (typeof console !== 'undefined' && console.warn) {
|
|
11
|
+
console.warn('[' + loggerName + '] could not set localStorage key ' + entry[0], error);
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
});
|
|
15
|
+
}
|
package/src/dom.js
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
export function installDomCleaner(selectors, loggerName) {
|
|
2
|
+
if (typeof document === 'undefined' || !Array.isArray(selectors) || selectors.length === 0) return;
|
|
3
|
+
|
|
4
|
+
injectStyle(selectors);
|
|
5
|
+
removeMatches(selectors, loggerName);
|
|
6
|
+
|
|
7
|
+
if (typeof MutationObserver === 'function' && document.documentElement) {
|
|
8
|
+
const observer = new MutationObserver(function onMutations() {
|
|
9
|
+
removeMatches(selectors, loggerName);
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
observer.observe(document.documentElement, {
|
|
13
|
+
childList: true,
|
|
14
|
+
subtree: true,
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function injectStyle(selectors) {
|
|
20
|
+
const style = document.createElement('style');
|
|
21
|
+
style.setAttribute('data-tizen-adblock', 'true');
|
|
22
|
+
style.textContent = selectors.join(',\n') + ' { display: none !important; visibility: hidden !important; }';
|
|
23
|
+
|
|
24
|
+
const target = document.head || document.documentElement;
|
|
25
|
+
if (target) target.appendChild(style);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function removeMatches(selectors, loggerName) {
|
|
29
|
+
selectors.forEach(function removeSelector(selector) {
|
|
30
|
+
try {
|
|
31
|
+
const nodes = document.querySelectorAll(selector);
|
|
32
|
+
for (let index = 0; index < nodes.length; index += 1) {
|
|
33
|
+
const node = nodes[index];
|
|
34
|
+
if (node && node.parentNode) {
|
|
35
|
+
node.parentNode.removeChild(node);
|
|
36
|
+
if (typeof console !== 'undefined' && console.debug) {
|
|
37
|
+
console.debug('[' + loggerName + '] removed DOM ad selector ' + selector);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
} catch (error) {
|
|
42
|
+
// Ignore selectors unsupported by older Tizen Chromium builds.
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
}
|
package/src/network.js
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
export function installNetworkBlocker(getBlockedReason, loggerName) {
|
|
2
|
+
patchFetch(getBlockedReason, loggerName);
|
|
3
|
+
patchXMLHttpRequest(getBlockedReason, loggerName);
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
function patchFetch(getBlockedReason, loggerName) {
|
|
7
|
+
if (typeof window === 'undefined' || typeof window.fetch !== 'function' || window.fetch.__tizenAdBlockPatched) {
|
|
8
|
+
return;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const originalFetch = window.fetch;
|
|
12
|
+
|
|
13
|
+
function blockedFetch(input, init) {
|
|
14
|
+
const url = getRequestUrl(input);
|
|
15
|
+
const reason = getBlockedReason(url);
|
|
16
|
+
if (reason) {
|
|
17
|
+
logBlock(loggerName, url, reason);
|
|
18
|
+
return Promise.resolve(createEmptyResponse());
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return originalFetch.apply(this, arguments);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
blockedFetch.__tizenAdBlockPatched = true;
|
|
25
|
+
blockedFetch.__tizenAdBlockOriginal = originalFetch;
|
|
26
|
+
window.fetch = blockedFetch;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function patchXMLHttpRequest(getBlockedReason, loggerName) {
|
|
30
|
+
if (typeof window === 'undefined' || typeof window.XMLHttpRequest !== 'function') return;
|
|
31
|
+
|
|
32
|
+
const proto = window.XMLHttpRequest.prototype;
|
|
33
|
+
if (proto.open.__tizenAdBlockPatched) return;
|
|
34
|
+
|
|
35
|
+
const originalOpen = proto.open;
|
|
36
|
+
|
|
37
|
+
proto.open = function patchedOpen(method, url) {
|
|
38
|
+
const requestUrl = getRequestUrl(url);
|
|
39
|
+
const reason = getBlockedReason(requestUrl);
|
|
40
|
+
if (reason) {
|
|
41
|
+
this.__tizenAdBlockBlocked = true;
|
|
42
|
+
this.__tizenAdBlockOriginalUrl = requestUrl;
|
|
43
|
+
logBlock(loggerName, requestUrl, reason);
|
|
44
|
+
const args = Array.prototype.slice.call(arguments);
|
|
45
|
+
args[1] = 'data:text/plain,';
|
|
46
|
+
return originalOpen.apply(this, args);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return originalOpen.apply(this, arguments);
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
proto.open.__tizenAdBlockPatched = true;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function getRequestUrl(input) {
|
|
56
|
+
if (input && typeof input === 'object' && typeof input.url === 'string') return input.url;
|
|
57
|
+
return String(input || '');
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function createEmptyResponse() {
|
|
61
|
+
if (typeof Response === 'function') {
|
|
62
|
+
return new Response('', {
|
|
63
|
+
status: 204,
|
|
64
|
+
statusText: 'No Content',
|
|
65
|
+
headers: { 'Content-Type': 'text/plain' },
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return {
|
|
70
|
+
ok: true,
|
|
71
|
+
status: 204,
|
|
72
|
+
statusText: 'No Content',
|
|
73
|
+
text: function text() { return Promise.resolve(''); },
|
|
74
|
+
json: function json() { return Promise.resolve({}); },
|
|
75
|
+
blob: function blob() { return Promise.resolve(new Blob([''])); },
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function logBlock(loggerName, url, reason) {
|
|
80
|
+
if (typeof console !== 'undefined' && console.debug) {
|
|
81
|
+
console.debug('[' + loggerName + '] blocked ' + url + ' (' + reason + ')');
|
|
82
|
+
}
|
|
83
|
+
}
|