Compare commits
4 commits
Author | SHA1 | Date | |
---|---|---|---|
e2ad6c0610 | |||
1a37e51dd4 | |||
c25869428f | |||
1a431c1fb7 |
|
@ -58,9 +58,51 @@ session.post('https://token.vrt.be',
|
|||
'ts': auth_info['signatureTimestamp'],
|
||||
'email': auth_info['profile']['email'],
|
||||
}).encode('utf-8'))
|
||||
|
||||
token = session.post('https://media-services-public.vrt.be/vualto-video-aggregator-web/rest/external/v1/tokens', headers={'Content-Type': 'application/json'}, data=b'').json()['vrtPlayerToken']
|
||||
```
|
||||
|
||||
### update sept 2021
|
||||
```
|
||||
session = requests.session()
|
||||
username = urllib.parse.quote('<vrtnu username here>', safe='')
|
||||
password = urllib.parse.quote('<vrtnu password here>', safe='')
|
||||
|
||||
data={
|
||||
'loginID': username,
|
||||
'password': password,
|
||||
'sessionExpiration': '-2',
|
||||
'APIKey': '3_qhEcPa5JGFROVwu5SWKqJ4mVOIkwlFNMSKwzPDAh8QZOtHqu6L4nD5Q7lk0eXOOG',
|
||||
'targetEnv': 'jssdk',
|
||||
}
|
||||
|
||||
|
||||
auth_info = requests.post('https://accounts.vrt.be/accounts.login', data=data).json()
|
||||
# get crsrf cookie
|
||||
session.get('https://token.vrt.be/vrtnuinitlogin?provider=site&destination=https://www.vrt.be/vrtnu/')
|
||||
# do login
|
||||
session.post('https://login.vrt.be/perform_login',
|
||||
data={
|
||||
'UID': auth_info['UID'],
|
||||
'UIDSignature': auth_info['UIDSignature'],
|
||||
'signatureTimestamp': auth_info['signatureTimestamp'],
|
||||
'client_id': 'vrtnu-site',
|
||||
'_csrf': session.cookies['OIDCXSRF'],
|
||||
#'email': auth_info['profile']['email'],
|
||||
})
|
||||
|
||||
|
||||
token = session.post('https://media-services-public.vrt.be/vualto-video-aggregator-web/rest/external/v1/tokens', headers={'Content-Type': 'application/json'}, data=b'').json()['vrtPlayerToken']
|
||||
```
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
vrtnu-site_profile_vt contains a token:
|
||||
|
||||
|
||||
data = {"identityToken":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL2xvZ2luLnZydC5iZSIsImF1ZCI6InZydG51LXNpdGUiLCJpYXQiOjE2MzE2NDkwNjAsImV4cCI6MTYzMTY1MjY2MCwic3ViIjoiOThmODNkNzMtNzlmNi00YTY4LWEzMmYtODBkMzlmMGRkZmE5IiwiYWMiOiIxNisiLCJ1c2VyX3N0YXR1cyI6IlZFUklGSUVEX0JFX1JFU0lERU5UIn0.NM4Fqg-6C_v5PMfizd-_5ZUfGD40D3S7wvG8WPpFJu8"}
|
||||
|
||||
|
||||
get video data
|
||||
|
|
|
@ -1,13 +1,11 @@
|
|||
#!/usr/bin/env ruby
|
||||
|
||||
print "What is the URL of your Apple Downloads resource?\nYou should find this on https://developer.apple.com/download/ \nURL:"
|
||||
print "What is the URL of your Apple Downloads resource?\nURL:"
|
||||
url = gets.strip
|
||||
|
||||
print "What is the ADCDownloadAuth cookie token:\nADCDownloadAuth: "
|
||||
token = gets.strip
|
||||
|
||||
print "Make sure you have aria2 installed (brew install aria2) "
|
||||
|
||||
command = "aria2c --header \"Host: adcdownload.apple.com\" --header \"Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\" --header \"Upgrade-Insecure-Requests: 1\" --header \"Cookie: ADCDownloadAuth=#{token}\" --header \"User-Agent: Mozilla/5.0 (iPhone; CPU iPhone OS 10_1 like Mac OS X) AppleWebKit/602.2.14 (KHTML, like Gecko) Version/10.0 Mobile/14B72 Safari/602.1\" --header \"Accept-Language: en-us\" -x 16 -s 16 #{url} -d ~/Downloads"
|
||||
|
||||
exec(command)
|
||||
|
|
|
@ -7,10 +7,10 @@
|
|||
objects = {
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
30A7823B26EE41EF00DAC1FB /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4F0CC7C251BE62B00E9EA74 /* ContentView.swift */; };
|
||||
B408F0DD251F6D180043E3A4 /* AsyncImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = B408F0DC251F6D180043E3A4 /* AsyncImage.swift */; };
|
||||
B408F0E5251F7AC50043E3A4 /* Just.swift in Sources */ = {isa = PBXBuildFile; fileRef = B408F0E4251F7AC50043E3A4 /* Just.swift */; };
|
||||
B4F0CC7B251BE62B00E9EA74 /* vrtnuApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4F0CC7A251BE62B00E9EA74 /* vrtnuApp.swift */; };
|
||||
B4F0CC7D251BE62B00E9EA74 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4F0CC7C251BE62B00E9EA74 /* ContentView.swift */; };
|
||||
B4F0CC7F251BE62F00E9EA74 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = B4F0CC7E251BE62F00E9EA74 /* Assets.xcassets */; };
|
||||
B4F0CC82251BE62F00E9EA74 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = B4F0CC81251BE62F00E9EA74 /* Preview Assets.xcassets */; };
|
||||
B4F0CC8D251BE62F00E9EA74 /* vrtnuTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4F0CC8C251BE62F00E9EA74 /* vrtnuTests.swift */; };
|
||||
|
@ -280,8 +280,8 @@
|
|||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
B408F0E5251F7AC50043E3A4 /* Just.swift in Sources */,
|
||||
B4F0CC7D251BE62B00E9EA74 /* ContentView.swift in Sources */,
|
||||
B4F0CCB7251D6B5400E9EA74 /* VrtNuLayout.swift in Sources */,
|
||||
30A7823B26EE41EF00DAC1FB /* ContentView.swift in Sources */,
|
||||
B4F0CC7B251BE62B00E9EA74 /* vrtnuApp.swift in Sources */,
|
||||
B4F0CCAA251BFD6F00E9EA74 /* video.swift in Sources */,
|
||||
B408F0DD251F6D180043E3A4 /* AsyncImage.swift in Sources */,
|
||||
|
|
|
@ -15,7 +15,7 @@ import TVUIKit
|
|||
struct ContentView: View {
|
||||
@ViewBuilder
|
||||
var body: some View {
|
||||
VRTNuView(vrtNu: VRTNu())
|
||||
VRTNuView(shows: VRTNu().getShows())
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -51,30 +51,55 @@ struct LoginView: View{
|
|||
})
|
||||
}
|
||||
}
|
||||
|
||||
struct VRTListView: View{
|
||||
var shows: [Show]
|
||||
var text: String = ""
|
||||
|
||||
var body: some View{
|
||||
List(shows, id: \.title){ show in
|
||||
NavigationLink(destination: ShowView(show: show)){
|
||||
HStack{
|
||||
AsyncImage(url: show.imageURL,placeholder: {
|
||||
//Image(name: "loading")
|
||||
Text("Loading...")
|
||||
}, image:{
|
||||
Image(uiImage:$0)
|
||||
.resizable()
|
||||
}).aspectRatio(contentMode: .fit).frame(width: 480, height:300)
|
||||
VStack(alignment: .leading){
|
||||
Text(show.title)
|
||||
.padding()
|
||||
Text(show.showURL.absoluteString)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
struct VRTNuView: View{
|
||||
var vrtNu: VRTNu
|
||||
var shows: [Show]
|
||||
@State private var searchText = ""
|
||||
|
||||
|
||||
var body: some View {
|
||||
|
||||
NavigationView(){
|
||||
List(vrtNu.getShows(), id: \.title){ show in
|
||||
NavigationLink(destination: ShowView(show: show)){
|
||||
HStack{
|
||||
AsyncImage(url: show.imageURL,placeholder: {
|
||||
//Image(name: "loading")
|
||||
Text("Loading...")
|
||||
}, image:{
|
||||
Image(uiImage:$0)
|
||||
.resizable()
|
||||
}).aspectRatio(contentMode: .fit).frame(width: 480, height:300)
|
||||
VStack(alignment: .leading){
|
||||
Text(show.title)
|
||||
.padding()
|
||||
Text(show.showURL.absoluteString)
|
||||
if #available(tvOS 15.0, *) {
|
||||
VRTListView(shows: shows.filter({ searchText.isEmpty ? true : $0.showName.contains(searchText) }))
|
||||
.searchable(text: $searchText)
|
||||
{
|
||||
ForEach(shows.filter({ searchText.isEmpty ? true : $0.showName.contains(searchText) })) { show in
|
||||
Text(show.title).searchCompletion(show.title)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Fallback on earlier versions
|
||||
VRTListView(shows: shows)
|
||||
}
|
||||
}.navigationBarTitle("Browse VRT Nu")
|
||||
}
|
||||
}.navigationTitle("Browse VRT Nu")//.listStyle(DefaultListStyle())
|
||||
}
|
||||
}
|
||||
struct ShowView: View {
|
||||
|
|
|
@ -52,15 +52,14 @@ struct Episode: Hashable, Comparable{
|
|||
let title = videojson.value(forKey: "title") as! String
|
||||
let targetURLs = videojson.value(forKey: "targetUrls") as! [NSDictionary]
|
||||
var videourl = targetURLs[0].value(forKey: "url") as! String
|
||||
|
||||
// it seams that the hls_aes stream has more changes of playing
|
||||
// TODO: pass all streams and switch stream if one fails?
|
||||
for i in 0 ..< targetURLs.count{
|
||||
if targetURLs[i].value(forKey: "type") as! String == "hls_aes"{
|
||||
videourl = targetURLs[i].value(forKey: "url") as! String
|
||||
var temptargeturl: String
|
||||
|
||||
for targeturl in targetURLs{
|
||||
temptargeturl = (targeturl["url"] as! String)
|
||||
if temptargeturl.contains("aes"){
|
||||
videourl = temptargeturl
|
||||
}
|
||||
}
|
||||
|
||||
print(videourl)
|
||||
//session.get('https://media-services-public.vrt.be/vualto-video-aggregator-web/rest/external/v1/videos/%s?vrtPlayerToken=%s&client=%s@PROD' %(video_id, token, clientid)).json()
|
||||
var video = Video(hlsUrl: URL(string: videourl)!,
|
||||
|
@ -106,12 +105,17 @@ struct Episode: Hashable, Comparable{
|
|||
}
|
||||
|
||||
func parseData(data: String, regexPattern: String) -> [String]{
|
||||
print("Parsing Data")
|
||||
print("regex")
|
||||
print(regexPattern)
|
||||
|
||||
let range = NSRange(location: 0, length: data.count)
|
||||
let regex = try! NSRegularExpression(pattern: regexPattern)
|
||||
let matches = regex.matches(in: data, range: range)
|
||||
let nsString = data as NSString
|
||||
//let output = Array(Set(matches.map {nsString.substring(with: $0.range)}))
|
||||
let output = matches.map {nsString.substring(with: $0.range)}
|
||||
print("parsedata output:")
|
||||
print(output)
|
||||
return output
|
||||
}
|
||||
|
@ -146,14 +150,20 @@ struct Season: Hashable, Comparable{
|
|||
|
||||
print("getting episodes for " + show.showName + " " + seasonName)
|
||||
let regexPattern = "vrtnu/a-z/" + show.showName + "/" + seasonName + "/([^\"]*)/"
|
||||
let imageregexPattern = "data-responsive-image=\".*(jpg|png)"
|
||||
let titleregexPattern = "\">(.*)(</a>|<br />)"
|
||||
let imageregexPattern = "images.vrt.be/.*(jpg|png)"
|
||||
let titleregexPattern = "\">(.*)(</a>|<br />|</h3>)"
|
||||
|
||||
let data = Just.get("https://www.vrt.be/vrtnu/a-z/" + show.showName + "/" + seasonName + ".lists.all-episodes/").text!
|
||||
print("getting url")
|
||||
let url = "https://www.vrt.be/vrtnu/a-z/" + show.showName + "/" + seasonName + ".episodes-list/"
|
||||
print(url)
|
||||
let data = Just.get(url).text!
|
||||
let output = parseData(data: data, regexPattern: regexPattern)
|
||||
let imageoutput = parseData(data: data, regexPattern: imageregexPattern)
|
||||
let titleoutput = parseData(data: data, regexPattern: titleregexPattern)
|
||||
print(data)
|
||||
print(output)
|
||||
print(imageoutput)
|
||||
print(titleoutput)
|
||||
|
||||
var episode: String
|
||||
var myepisodes: [Episode]
|
||||
|
@ -163,8 +173,8 @@ struct Season: Hashable, Comparable{
|
|||
episode = output[i * 2].replacingOccurrences(of: "vrtnu/a-z/" + show.showName + "/" + seasonName + "/", with: "").replacingOccurrences(of: "/", with: "")
|
||||
print(episode)
|
||||
print(seasonName)
|
||||
let image = URL(string: imageoutput[i].replacingOccurrences(of: "https:", with: "").replacingOccurrences(of: "http:", with: "").replacingOccurrences(of: "data-responsive-image=\"", with: "https:"))!
|
||||
let title = titleoutput[i].replacingOccurrences(of: "\">", with: "").replacingOccurrences(of: "<br />", with: "").replacingOccurrences(of: "</a>", with: "")
|
||||
let image = URL(string: imageoutput[i].replacingOccurrences(of: "https:", with: "").replacingOccurrences(of: "http:", with: "").replacingOccurrences(of: "images.vrt.be/", with: "https://images.vrt.be/"))!
|
||||
let title = titleoutput[i].replacingOccurrences(of: "\">", with: "").replacingOccurrences(of: "<br />", with: "").replacingOccurrences(of: "</a>", with: "").replacingOccurrences(of: "</h3>", with: "")
|
||||
myepisodes.append(Episode(season: self, episodeName: episode, title: title, imageURL: image))
|
||||
}
|
||||
|
||||
|
@ -174,7 +184,9 @@ struct Season: Hashable, Comparable{
|
|||
|
||||
}
|
||||
|
||||
struct Show: Hashable, Comparable{
|
||||
struct Show: Hashable, Comparable, Identifiable{
|
||||
let id = UUID()
|
||||
|
||||
|
||||
static func < (lhs: Show, rhs: Show) -> Bool {
|
||||
//return lhs.showName < rhs.showName
|
||||
|
@ -239,23 +251,31 @@ struct Show: Hashable, Comparable{
|
|||
|
||||
func getSeasons() -> [Season]{
|
||||
//`re.findall('value="#parsys_container_banner_%s_(.*)">' % show, requests.get('https://www.vrt.be/vrtnu/a-z/%s/' % show).text)`
|
||||
let regexPattern = "value=\"#parsys_container_banner_(" + showName + "_)?(.*)\">"
|
||||
var regexPattern = "id=\"parsys_container_banner_(.*)_title\">"
|
||||
print("getting seasons from " + showURL.absoluteString)
|
||||
print("filtering with " + regexPattern)
|
||||
|
||||
let output = parseData(data: Just.get(showURL).text!, regexPattern: regexPattern)
|
||||
|
||||
let data = Just.get(showURL).text!
|
||||
var output = parseData(data: data, regexPattern: regexPattern)
|
||||
if output.count == 0 {
|
||||
regexPattern = "id=\"parsys_container_episodes-list_(.*)_title\">"
|
||||
output = parseData(data: data, regexPattern: regexPattern)
|
||||
}
|
||||
if output.count == 0 {
|
||||
regexPattern = "value=\"#parsys_container_banner_(.*)\">"
|
||||
output = parseData(data: data, regexPattern: regexPattern)
|
||||
}
|
||||
var season: String
|
||||
var myseasons: [Season]
|
||||
myseasons = []
|
||||
for i in 0 ..< output.count{
|
||||
print(output[i])
|
||||
season = output[i].replacingOccurrences(of: "value=\"#parsys_container_banner_", with: "")
|
||||
.replacingOccurrences(of: showName + "_", with: "").replacingOccurrences(of: "\">", with: "")
|
||||
season = output[i].replacingOccurrences(of: "id=\"parsys_container_episodes-list_", with: "").replacingOccurrences(of: "_title", with: "").replacingOccurrences(of: "id=\"parsys_container_banner_", with: "").replacingOccurrences(of: "value=\"#parsys_container_banner_", with: "").replacingOccurrences(of: "\">", with: "")
|
||||
print(season)
|
||||
print(showName)
|
||||
myseasons.append(Season(show: self, seasonName: season, title: season))
|
||||
}
|
||||
|
||||
myseasons.sort()
|
||||
return myseasons
|
||||
}
|
||||
|
@ -275,9 +295,10 @@ extension NSTextCheckingResult {
|
|||
|
||||
struct VRTNu: Hashable {
|
||||
|
||||
let regexPattern = "a href=\"/vrtnu/a-z/(.*).relevant"
|
||||
let regexPattern = "a href=\"/vrtnu/a-z/(.*)/\">"
|
||||
let imageregexPattern = "data-responsive-image=\".*(jpg|png)"
|
||||
let titleregexPattern = ".relevant/\">(.*)</a>"
|
||||
let titleregexPattern = "a href=\"/vrtnu/a-z/.*/\">(.*)</a>"
|
||||
|
||||
|
||||
func getToken() -> String {
|
||||
///token = session.post('https://media-services-public.vrt.be/vualto-video-aggregator-web/rest/external/v1/tokens', headers={'Content-Type': 'application/json'}, data=b'').json()['vrtPlayerToken']
|
||||
|
@ -292,14 +313,15 @@ struct VRTNu: Hashable {
|
|||
|
||||
func login(username: String, password:String) -> Bool {
|
||||
let auth_data = [
|
||||
"ApiKey": "3_0Z2HujMtiWq_pkAjgnS2Md2E11a1AwZjYiBETtwNE-EoEHDINgtnvcAOpNgmrVGy",
|
||||
"ApiKey": "3_qhEcPa5JGFROVwu5SWKqJ4mVOIkwlFNMSKwzPDAh8QZOtHqu6L4nD5Q7lk0eXOOG",
|
||||
"targetEnv": "jssdk",
|
||||
"loginID": username,
|
||||
"password": password,
|
||||
"authMode": "cookie",
|
||||
"sessionExpiration": "-2",
|
||||
]
|
||||
let auth_json = just.post("https://accounts.eu1.gigya.com/accounts.login", data: auth_data).json!
|
||||
let auth_json = just.post("https://accounts.vrt.be/accounts.login", data: auth_data).json!
|
||||
let auth_info = auth_json as! NSDictionary
|
||||
print("auth_info")
|
||||
print(auth_info)
|
||||
if auth_info.object(forKey: "statusCode") != nil{
|
||||
if auth_info.value(forKey: "statusCode") as! Int == 403
|
||||
|
@ -308,19 +330,19 @@ struct VRTNu: Hashable {
|
|||
return false
|
||||
}
|
||||
}
|
||||
// no token is returnd but necessary cookies are set
|
||||
just.post("https://token.vrt.be", json:[
|
||||
"uid": auth_info.value(forKey: "UID"),
|
||||
"uidsig": auth_info.value(forKey: "UIDSignature"),
|
||||
"ts": auth_info.value(forKey: "signatureTimestamp"),
|
||||
// get csrf
|
||||
// tokensession.get('https://token.vrt.be/vrtnuinitlogin?provider=site&destination=https://www.vrt.be/vrtnu/')
|
||||
let csrf = just.get("https://token.vrt.be/vrtnuinitlogin?provider=site&destination=https://www.vrt.be/vrtnu/").cookies["OIDCXSRF"]
|
||||
|
||||
just.post("https://login.vrt.be/perform_login", data :[
|
||||
"UID": auth_info.value(forKey: "UID")!,
|
||||
"UIDSignature": auth_info.value(forKey: "UIDSignature")!,
|
||||
"signatureTimestamp": auth_info.value(forKey: "signatureTimestamp")!,
|
||||
"client_id": "vrtnu-site",
|
||||
"_csrf": csrf!.value,
|
||||
//"email": auth_info.value(forKey: "profile"['email'],
|
||||
"email": username
|
||||
],
|
||||
headers: [
|
||||
"Conetnt-Type": "application/json",
|
||||
"Referer": "https://www.vrt.be/vrtnu/",
|
||||
//"email": username
|
||||
]
|
||||
|
||||
)
|
||||
print("authenticated")
|
||||
return true
|
||||
|
@ -335,6 +357,7 @@ struct VRTNu: Hashable {
|
|||
print("getting shows")
|
||||
|
||||
let data = Just.get("https://www.vrt.be/vrtnu/a-z/").text!
|
||||
print("show data")
|
||||
print(data)
|
||||
|
||||
let output = parseData(data: data, regexPattern: regexPattern)
|
||||
|
@ -347,9 +370,12 @@ struct VRTNu: Hashable {
|
|||
var myshows: [Show]
|
||||
myshows = []
|
||||
for i in 0 ..< output.count{
|
||||
show = output[i].replacingOccurrences(of: ".relevant", with: "").replacingOccurrences(of: "a href=\"/vrtnu/a-z/", with: "")
|
||||
show = output[i].replacingOccurrences(of: "/\">", with: "").replacingOccurrences(of: "a href=\"/vrtnu/a-z/", with: "")
|
||||
image = imageoutput[i].replacingOccurrences(of: "https:", with: "").replacingOccurrences(of: "http:", with: "").replacingOccurrences(of: "data-responsive-image=\"", with: "https:")
|
||||
title = titleoutput[i].replacingOccurrences(of: ".relevant/\">", with: "").replacingOccurrences(of: "</a>", with: "")
|
||||
title = titleoutput[i].replacingOccurrences(of: "a href=\"/vrtnu/a-z/" + show + "/\">", with: "").replacingOccurrences(of: "</a>", with: "")
|
||||
print(show)
|
||||
print(image)
|
||||
print(title)
|
||||
myshows.append(Show(vrtNu: self, showName: show, title: title, imageURL: URL(string: image)!))
|
||||
}
|
||||
myshows.sort()
|
||||
|
|
38
vrtnu/vrtnu/dependencies/SearchBar.swift
Normal file
38
vrtnu/vrtnu/dependencies/SearchBar.swift
Normal file
|
@ -0,0 +1,38 @@
|
|||
//
|
||||
// SearchBar.swift
|
||||
// vrtnu
|
||||
//
|
||||
// Created by Jens Timmerman on 29/10/2020.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct SearchBar: View {
|
||||
@Binding var text: String
|
||||
|
||||
@State private var isEditing = false
|
||||
|
||||
var body: some View {
|
||||
HStack {
|
||||
|
||||
TextField("Search ...", text: $text)
|
||||
.padding(7)
|
||||
.padding(.horizontal, 25)
|
||||
.cornerRadius(8).overlay(
|
||||
HStack {
|
||||
Image(systemName: "magnifyingglass")
|
||||
.foregroundColor(.gray)
|
||||
.frame(minWidth: 0, maxWidth: .infinity, alignment: .leading)
|
||||
.padding(.leading, 8)
|
||||
}
|
||||
)
|
||||
.padding(.horizontal, 10)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct SearchBar_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
SearchBar(text: .constant(""))
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue