├── .gitignore ├── LICENSE ├── README.md └── main.go /.gitignore: -------------------------------------------------------------------------------- 1 | ### Go ### 2 | # Binaries for programs and plugins 3 | *.exe 4 | *.exe~ 5 | *.dll 6 | *.so 7 | *.dylib 8 | 9 | # Test binary, built with `go test -c` 10 | *.test 11 | 12 | # Output of the go coverage tool, specifically when used with LiteIDE 13 | *.out 14 | 15 | ### Go Patch ### 16 | /vendor/ 17 | /Godeps/ 18 | 19 | ### JetBrains ### 20 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm 21 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 22 | 23 | # User-specific stuff 24 | .idea/**/workspace.xml 25 | .idea/**/tasks.xml 26 | .idea/**/usage.statistics.xml 27 | .idea/**/dictionaries 28 | .idea/**/shelf 29 | 30 | # Generated files 31 | .idea/**/contentModel.xml 32 | 33 | # Sensitive or high-churn files 34 | .idea/**/dataSources/ 35 | .idea/**/dataSources.ids 36 | .idea/**/dataSources.local.xml 37 | .idea/**/sqlDataSources.xml 38 | .idea/**/dynamic.xml 39 | .idea/**/uiDesigner.xml 40 | .idea/**/dbnavigator.xml 41 | 42 | # Gradle 43 | .idea/**/gradle.xml 44 | .idea/**/libraries 45 | 46 | # Gradle and Maven with auto-import 47 | # When using Gradle or Maven with auto-import, you should exclude module files, 48 | # since they will be recreated, and may cause churn. Uncomment if using 49 | # auto-import. 50 | # .idea/modules.xml 51 | # .idea/*.iml 52 | # .idea/modules 53 | 54 | # CMake 55 | cmake-build-*/ 56 | 57 | # Mongo Explorer plugin 58 | .idea/**/mongoSettings.xml 59 | 60 | # File-based project format 61 | *.iws 62 | 63 | # IntelliJ 64 | out/ 65 | 66 | # mpeltonen/sbt-idea plugin 67 | .idea_modules/ 68 | 69 | # JIRA plugin 70 | atlassian-ide-plugin.xml 71 | 72 | # Cursive Clojure plugin 73 | .idea/replstate.xml 74 | 75 | # Crashlytics plugin (for Android Studio and IntelliJ) 76 | com_crashlytics_export_strings.xml 77 | crashlytics.properties 78 | crashlytics-build.properties 79 | fabric.properties 80 | 81 | # Editor-based Rest Client 82 | .idea/httpRequests 83 | 84 | # Android studio 3.1+ serialized cache file 85 | .idea/caches/build_file_checksums.ser 86 | 87 | ### JetBrains Patch ### 88 | # Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721 89 | 90 | # *.iml 91 | # modules.xml 92 | # .idea/misc.xml 93 | # *.ipr 94 | 95 | # Sonarlint plugin 96 | .idea/sonarlint 97 | 98 | ### macOS ### 99 | # General 100 | .DS_Store 101 | .AppleDouble 102 | .LSOverride 103 | 104 | # Icon must end with two \r 105 | Icon 106 | 107 | # Thumbnails 108 | ._* 109 | 110 | # Files that might appear in the root of a volume 111 | .DocumentRevisions-V100 112 | .fseventsd 113 | .Spotlight-V100 114 | .TemporaryItems 115 | .Trashes 116 | .VolumeIcon.icns 117 | .com.apple.timemachine.donotpresent 118 | 119 | # Directories potentially created on remote AFP share 120 | .AppleDB 121 | .AppleDesktop 122 | Network Trash Folder 123 | Temporary Items 124 | .apdisk 125 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE 2 | Version 2, December 2004 3 | 4 | Copyright (C) 2019 Kyon Li 5 | 6 | Everyone is permitted to copy and distribute verbatim or modified 7 | copies of this license document, and changing it is allowed as long 8 | as the name is changed. 9 | 10 | DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE 11 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 12 | 13 | 0. You just DO WHAT THE FUCK YOU WANT TO. 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 移动魔百盒直播源生成m3u8播放列表 2 | 3 | ## 使用方法 4 | 5 | 参照 [@zhantong](https://github.com/zhantong) 的文章 [苏州移动IPTV抓包](https://www.polarxiong.com/archives/苏州移动IPTV抓包.html) 获取频道列表接口地址和基础URL 6 | 7 | Usage of ottcn2m3u8: 8 | -api string 9 | API URL to fetch channel list. (default "http://looktvepg.jsa.bcs.ottcn.com:8080/ysten-lvoms-epg/epg/getChannelIndexs.shtml?deviceGroupId=1697") 10 | -base string 11 | Base URL for stream. (default "http://183.207.248.71:80/cntv/live1") 12 | -h This help. 13 | -o string 14 | Output file path. (default "channel.m3u8") 15 | -v Verbose mode. 16 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "flag" 6 | "fmt" 7 | "io" 8 | "log" 9 | "net/http" 10 | "net/url" 11 | "os" 12 | "regexp" 13 | "sort" 14 | "strconv" 15 | ) 16 | 17 | var ( 18 | help = flag.Bool("h", false, "This help.") 19 | verbose = flag.Bool("v", false, "Verbose mode.") 20 | baseURL = flag.String("base", "http://183.207.248.71:80/cntv/live1", "Base URL for stream.") 21 | apiURL = flag.String("api", "http://looktvepg.jsa.bcs.ottcn.com:8080/ysten-lvoms-epg/epg/getChannelIndexs.shtml?deviceGroupId=1697", "API URL to fetch channel list.") 22 | outputFile = flag.String("o", "channel.m3u8", "Output file path.") 23 | ) 24 | 25 | type Channel struct { 26 | UUID string `json:"uuid"` 27 | ChannelName string `json:"channelName"` 28 | ChannelIcon string `json:"channelIcon"` 29 | } 30 | 31 | func (c *Channel) toString() string { 32 | u, _ := url.Parse(*baseURL + "/" + c.ChannelName + "/" + c.UUID) 33 | return fmt.Sprintf("#EXTINF:-1,%s\n%s\n", c.ChannelName, u.String()) 34 | } 35 | 36 | func getJSONContent() map[string]Channel { 37 | fmt.Println("Fetching channel list...") 38 | 39 | resp, err := http.Get(*apiURL) 40 | if err != nil { 41 | log.Fatalln(err) 42 | } 43 | defer resp.Body.Close() 44 | 45 | if resp.StatusCode != http.StatusOK { 46 | log.Fatalf("api error: %s", resp.Status) 47 | } 48 | 49 | var result map[string]Channel 50 | if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { 51 | log.Fatalln(err) 52 | } 53 | return result 54 | } 55 | 56 | func generateChannelList(json *map[string]Channel) []Channel { 57 | fmt.Println("Parsing...") 58 | 59 | keySort := make(map[int]string, len(*json)) 60 | for key := range *json { 61 | number := stringArrayToString(regexp.MustCompile("[0-9]").FindAllStringSubmatch(key, -1)) 62 | i, err := strconv.Atoi(number) 63 | if err != nil { 64 | log.Fatalf("failed to parse channel list: %v", err) 65 | } 66 | keySort[i] = key 67 | } 68 | 69 | subKeys := make([]int, 0, len(keySort)) 70 | for key := range keySort { 71 | subKeys = append(subKeys, key) 72 | } 73 | sort.Ints(subKeys) 74 | 75 | channels := make([]Channel, 0, len(keySort)) 76 | for _, subKey := range subKeys { 77 | key := keySort[subKey] 78 | channels = append(channels, (*json)[key]) 79 | } 80 | 81 | return channels 82 | } 83 | 84 | func stringArrayToString(array [][]string) string { 85 | line := "" 86 | for _, sa := range array { 87 | for _, s := range sa { 88 | line += s 89 | } 90 | } 91 | return line 92 | } 93 | 94 | func generateM3U8(channels *[]Channel) { 95 | fmt.Println("Generating m3u8...") 96 | 97 | content := fmt.Sprintf("#EXTM3U\n\n") 98 | for _, c := range *channels { 99 | content += c.toString() 100 | if *verbose { 101 | fmt.Println(c.ChannelName) 102 | } 103 | } 104 | 105 | f, err := os.Create(*outputFile) 106 | if err != nil { 107 | log.Fatalln(err) 108 | } 109 | defer f.Close() 110 | 111 | _, err = io.WriteString(f, content) 112 | if err != nil { 113 | log.Fatalln(err) 114 | } 115 | 116 | fmt.Printf("Done, all saved to %s\n", *outputFile) 117 | } 118 | 119 | func main() { 120 | flag.Parse() 121 | 122 | if *help { 123 | flag.Usage() 124 | return 125 | } 126 | 127 | j := getJSONContent() 128 | l := generateChannelList(&j) 129 | generateM3U8(&l) 130 | } 131 | --------------------------------------------------------------------------------