Browse Source

added Airtime API and progress bar

yattoz 5 years ago
parent
commit
c03a002448

+ 2 - 2
app/build.gradle View File

29
         applicationId "fr.forum_thalie.tsumugi"
29
         applicationId "fr.forum_thalie.tsumugi"
30
         minSdkVersion 16
30
         minSdkVersion 16
31
         targetSdkVersion 29
31
         targetSdkVersion 29
32
-        versionCode 100
33
-        versionName "1.0.0"
32
+        versionCode 110
33
+        versionName "1.1.0"
34
         testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
34
         testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
35
         vectorDrawables.useSupportLibrary = true
35
         vectorDrawables.useSupportLibrary = true
36
     }
36
     }

+ 0 - 3
app/src/main/AndroidManifest.xml View File

31
                 <action android:name="android.media.browse.MediaBrowserService" />
31
                 <action android:name="android.media.browse.MediaBrowserService" />
32
             </intent-filter>
32
             </intent-filter>
33
         </service>
33
         </service>
34
-        <service android:name=".streamerNotificationService.StreamerMonitorService"
35
-            android:enabled="true"
36
-            android:exported="true"/>
37
 
34
 
38
         <receiver android:name=".BootBroadcastReceiver"
35
         <receiver android:name=".BootBroadcastReceiver"
39
             android:directBootAware="true"
36
             android:directBootAware="true"

+ 4 - 0
app/src/main/java/fr/forum_thalie/tsumugi/BootBroadcastReceiver.kt View File

22
 
22
 
23
         if (arg1.getStringExtra("action") == "$tag.${Actions.PLAY_OR_FALLBACK.name}" )
23
         if (arg1.getStringExtra("action") == "$tag.${Actions.PLAY_OR_FALLBACK.name}" )
24
         {
24
         {
25
+
25
             RadioAlarm.instance.setNextAlarm(context) // schedule next alarm
26
             RadioAlarm.instance.setNextAlarm(context) // schedule next alarm
27
+            if (!PlayerStore.instance.isInitialized)
28
+                PlayerStore.instance.initApi()
26
             if (PlayerStore.instance.streamerName.value.isNullOrBlank())
29
             if (PlayerStore.instance.streamerName.value.isNullOrBlank())
27
                 PlayerStore.instance.initPicture(context)
30
                 PlayerStore.instance.initPicture(context)
31
+            // TODO: add initialization for programme.
28
 
32
 
29
             val i = Intent(context, RadioService::class.java)
33
             val i = Intent(context, RadioService::class.java)
30
             i.putExtra("action", Actions.PLAY_OR_FALLBACK.name)
34
             i.putExtra("action", Actions.PLAY_OR_FALLBACK.name)

+ 3 - 0
app/src/main/java/fr/forum_thalie/tsumugi/MainActivity.kt View File

129
         colorGreenListCompat = (ResourcesCompat.getColorStateList(resources, R.color.button_green_compat, null))
129
         colorGreenListCompat = (ResourcesCompat.getColorStateList(resources, R.color.button_green_compat, null))
130
         colorAccent = (ResourcesCompat.getColor(resources, R.color.colorAccent, null))
130
         colorAccent = (ResourcesCompat.getColor(resources, R.color.colorAccent, null))
131
 
131
 
132
+        PlayerStore.instance.initUrl(this)
133
+        PlayerStore.instance.initApi()
134
+
132
         // Post-UI Launch
135
         // Post-UI Launch
133
         if (PlayerStore.instance.isInitialized)
136
         if (PlayerStore.instance.isInitialized)
134
         {
137
         {

+ 27 - 0
app/src/main/java/fr/forum_thalie/tsumugi/RadioService.kt View File

124
 
124
 
125
     private val titleObserver = Observer<String> {
125
     private val titleObserver = Observer<String> {
126
         // We're checking if a new song arrives. If so, we put the currentSong in Lp and update the backup.
126
         // We're checking if a new song arrives. If so, we put the currentSong in Lp and update the backup.
127
+
128
+        if (PlayerStore.instance.playbackState.value == PlaybackStateCompat.STATE_PLAYING)
129
+        {
130
+            Log.d(tag, radioTag + "SONG CHANGED AND PLAYING")
131
+            // we activate latency compensation only if it's been at least 2 songs...
132
+            when {
133
+                PlayerStore.instance.isStreamDown -> {
134
+                    // if we reach here, it means that the observer has been called by a new song and that the stream was down previously.
135
+                    // so the stream is now back to normal.
136
+                    PlayerStore.instance.isStreamDown = false
137
+                    PlayerStore.instance.initApi()
138
+                }
139
+                PlayerStore.instance.currentSong.title.value == noConnectionValue -> {
140
+                    PlayerStore.instance.isStreamDown = true
141
+                }
142
+                else -> {
143
+                    PlayerStore.instance.fetchApi(numberOfSongs >= 2)
144
+                }
145
+            }
146
+        }
147
+
127
         if (PlayerStore.instance.currentSong != PlayerStore.instance.currentSongBackup
148
         if (PlayerStore.instance.currentSong != PlayerStore.instance.currentSongBackup
128
             && it != noConnectionValue)
149
             && it != noConnectionValue)
129
         {
150
         {
163
 
184
 
164
         preferenceStore = PreferenceManager.getDefaultSharedPreferences(this)
185
         preferenceStore = PreferenceManager.getDefaultSharedPreferences(this)
165
 
186
 
187
+        // start ticker for when the player is stopped
188
+        val periodString = PreferenceManager.getDefaultSharedPreferences(this).getString("fetchPeriod", "10") ?: "10"
189
+        val period: Long = Integer.parseInt(periodString).toLong()
190
+        if (period > 0)
191
+            apiTicker.schedule(ApiFetchTick(), 0, period * 1000)
192
+
166
         // Define managers
193
         // Define managers
167
         telephonyManager = getSystemService(Context.TELEPHONY_SERVICE) as TelephonyManager
194
         telephonyManager = getSystemService(Context.TELEPHONY_SERVICE) as TelephonyManager
168
         telephonyManager?.listen(phoneStateListener, PhoneStateListener.LISTEN_CALL_STATE)
195
         telephonyManager?.listen(phoneStateListener, PhoneStateListener.LISTEN_CALL_STATE)

+ 10 - 0
app/src/main/java/fr/forum_thalie/tsumugi/Tickers.kt View File

1
 package fr.forum_thalie.tsumugi
1
 package fr.forum_thalie.tsumugi
2
 
2
 
3
+import android.support.v4.media.session.PlaybackStateCompat
3
 import fr.forum_thalie.tsumugi.playerstore.PlayerStore
4
 import fr.forum_thalie.tsumugi.playerstore.PlayerStore
4
 import java.util.*
5
 import java.util.*
5
 
6
 
7
+class ApiFetchTick  : TimerTask() {
8
+    override fun run() {
9
+        if (PlayerStore.instance.playbackState.value == PlaybackStateCompat.STATE_STOPPED)
10
+        {
11
+            PlayerStore.instance.fetchApi()
12
+        }
13
+    }
14
+}
15
+
6
 class Tick  : TimerTask() {
16
 class Tick  : TimerTask() {
7
     override fun run() {
17
     override fun run() {
8
         PlayerStore.instance.currentTime.postValue(PlayerStore.instance.currentTime.value!! + 500)
18
         PlayerStore.instance.currentTime.postValue(PlayerStore.instance.currentTime.value!! + 500)

+ 114 - 0
app/src/main/java/fr/forum_thalie/tsumugi/playerstore/PlayerStore.kt View File

7
 import android.util.Log
7
 import android.util.Log
8
 import androidx.lifecycle.MutableLiveData
8
 import androidx.lifecycle.MutableLiveData
9
 import fr.forum_thalie.tsumugi.*
9
 import fr.forum_thalie.tsumugi.*
10
+import org.json.JSONObject
11
+import java.net.URL
12
+import java.text.ParseException
13
+import java.text.SimpleDateFormat
14
+import java.util.*
15
+import kotlin.collections.ArrayList
10
 
16
 
11
 class PlayerStore {
17
 class PlayerStore {
12
 
18
 
19
+    private lateinit var urlToScrape: String
13
     val isPlaying: MutableLiveData<Boolean> = MutableLiveData()
20
     val isPlaying: MutableLiveData<Boolean> = MutableLiveData()
14
     val isServiceStarted: MutableLiveData<Boolean> = MutableLiveData()
21
     val isServiceStarted: MutableLiveData<Boolean> = MutableLiveData()
15
     val volume: MutableLiveData<Int> = MutableLiveData()
22
     val volume: MutableLiveData<Int> = MutableLiveData()
27
     val listenersCount: MutableLiveData<Int> = MutableLiveData()
34
     val listenersCount: MutableLiveData<Int> = MutableLiveData()
28
     var latencyCompensator : Long = 0
35
     var latencyCompensator : Long = 0
29
     var isInitialized: Boolean = false
36
     var isInitialized: Boolean = false
37
+    var isStreamDown: Boolean = false
30
 
38
 
31
     init {
39
     init {
32
         playbackState.value = PlaybackStateCompat.STATE_STOPPED
40
         playbackState.value = PlaybackStateCompat.STATE_STOPPED
43
         listenersCount.value = 0
51
         listenersCount.value = 0
44
     }
52
     }
45
 
53
 
54
+    fun initUrl(c: Context)
55
+    {
56
+        urlToScrape = c.getString(R.string.API_URL)
57
+    }
58
+
59
+    private fun getTimestamp(s: String) : Long
60
+    {
61
+        val dateFormat = SimpleDateFormat("yyyy-MM-dd hh:mm:ss", Locale.getDefault())
62
+        try {
63
+            val t: Date? = dateFormat.parse(s)
64
+            return t!!.time
65
+        } catch (e: ParseException) {
66
+            e.printStackTrace()
67
+        }
68
+        return 0
69
+    }
70
+
71
+
72
+    // ##################################################
73
+    // ################# API FUNCTIONS ##################
74
+    // ##################################################
75
+
76
+    private fun updateApi(res: JSONObject, isCompensatingLatency : Boolean = false) {
77
+        // If we're not in PLAYING state, update title / artist metadata. If we're playing, the ICY will take care of that.
78
+
79
+        val resMain = res.getJSONObject("tracks").getJSONObject("current")
80
+        if (playbackState.value != PlaybackStateCompat.STATE_PLAYING || currentSong.title.value.isNullOrEmpty()
81
+            || currentSong.title.value == noConnectionValue)
82
+            currentSong.setTitleArtist(resMain.getString("name"))
83
+
84
+        val starts = getTimestamp(resMain.getString("starts"))
85
+        val ends = getTimestamp(resMain.getString("ends"))
86
+
87
+        if (currentSong.startTime.value != starts)
88
+            currentSong.startTime.value = starts
89
+
90
+        currentSong.stopTime.value = ends
91
+
92
+        // I noticed that the server has a big (3 to 9 seconds !!) offset for current time.
93
+        // we can measure it when the player is playing, to compensate it and have our progress bar perfectly timed
94
+        // latencyCompensator is set to null when beginPlaying() (we can't measure it at the moment we start playing, since we're in the middle of a song),
95
+        // at this moment, we set it to 0. Then, next time the updateApi is called when we're playing, we measure the latency and we set out latencyComparator.
96
+        if(isCompensatingLatency)
97
+        {
98
+            latencyCompensator = getTimestamp(res.getJSONObject("station").getString("schedulerTime")) - (currentSong.startTime.value ?: getTimestamp(res.getJSONObject("station").getString("schedulerTime")))
99
+            Log.d(tag, playerStoreTag +  "latency compensator set to ${(latencyCompensator).toFloat()/1000} s")
100
+        }
101
+        currentTime.value = getTimestamp(res.getJSONObject("station").getString("schedulerTime")) - (latencyCompensator)
102
+
103
+        /*
104
+        val listeners = resMain.getInt("listeners")
105
+        listenersCount.value = listeners
106
+        Log.d(tag, playerStoreTag +  "store updated")
107
+         */
108
+    }
109
+
110
+    private val scrape : (Any?) -> String =
111
+        {
112
+            URL(urlToScrape).readText()
113
+        }
114
+
115
+    /* initApi is called :
116
+        - at startup
117
+        - when a streamer changes.
118
+        the idea is to fetch the queue when a streamer changes (potentially Hanyuu), and at startup.
119
+        The Last Played is only fetched if it's empty (so, only at startup), not when a streamer changes.
120
+     */
121
+    fun initApi()
122
+    {
123
+        val post : (parameter: Any?) -> Unit = {
124
+            val result = JSONObject(it as String)
125
+            if (result.has("tracks"))
126
+            {
127
+                updateApi(result)
128
+                currentSongBackup.copy(currentSong)
129
+
130
+                isQueueUpdated.value = true
131
+
132
+                isLpUpdated.value = true
133
+            }
134
+            isInitialized = true
135
+        }
136
+        Async(scrape, post)
137
+    }
138
+
139
+    fun fetchApi(isCompensatingLatency: Boolean = false) {
140
+        val post: (parameter: Any?) -> Unit = {
141
+            val result = JSONObject(it as String)
142
+            if (!result.isNull("tracks"))
143
+            {
144
+                updateApi(result, isCompensatingLatency)
145
+            }
146
+        }
147
+        Async(scrape, post)
148
+    }
149
+
150
+    private fun extractSong(songJSON: JSONObject) : Song {
151
+        val song = Song()
152
+        song.setTitleArtist(songJSON.getString("meta"))
153
+        song.startTime.value = songJSON.getLong("timestamp")
154
+        song.stopTime.value = song.startTime.value
155
+        song.type.value = songJSON.getInt("type")
156
+        return song
157
+    }
158
+
159
+
46
     // ##################################################
160
     // ##################################################
47
     // ############## QUEUE / LP FUNCTIONS ##############
161
     // ############## QUEUE / LP FUNCTIONS ##############
48
     // ##################################################
162
     // ##################################################

+ 3 - 3
app/src/main/res/layout/fragment_nowplaying.xml View File

305
             android:progressDrawable="@drawable/progress_bar_progress"
305
             android:progressDrawable="@drawable/progress_bar_progress"
306
             app:layout_constraintBottom_toTopOf="@id/play_pause"
306
             app:layout_constraintBottom_toTopOf="@id/play_pause"
307
             tools:layout_editor_absoluteX="0dp"
307
             tools:layout_editor_absoluteX="0dp"
308
-            android:visibility="gone"/>
308
+            android:visibility="visible"/>
309
 
309
 
310
         <!-- REMOVE VISIBILITY GONE IF YOU HAVE TIME VALUES TO DISPLAY THE PROGRESS BAR -->
310
         <!-- REMOVE VISIBILITY GONE IF YOU HAVE TIME VALUES TO DISPLAY THE PROGRESS BAR -->
311
         <TextView
311
         <TextView
318
             android:textAlignment="textEnd"
318
             android:textAlignment="textEnd"
319
             app:layout_constraintEnd_toEndOf="@id/progressBar"
319
             app:layout_constraintEnd_toEndOf="@id/progressBar"
320
             app:layout_constraintTop_toBottomOf="@id/progressBar"
320
             app:layout_constraintTop_toBottomOf="@id/progressBar"
321
-            android:visibility="gone"/>
321
+            android:visibility="visible"/>
322
 
322
 
323
         <!-- REMOVE VISIBILITY GONE IF YOU HAVE TIME VALUES TO DISPLAY THE PROGRESS BAR -->
323
         <!-- REMOVE VISIBILITY GONE IF YOU HAVE TIME VALUES TO DISPLAY THE PROGRESS BAR -->
324
         <TextView
324
         <TextView
331
             android:textAlignment="textStart"
331
             android:textAlignment="textStart"
332
             app:layout_constraintStart_toStartOf="@id/progressBar"
332
             app:layout_constraintStart_toStartOf="@id/progressBar"
333
             app:layout_constraintTop_toBottomOf="@id/progressBar"
333
             app:layout_constraintTop_toBottomOf="@id/progressBar"
334
-            android:visibility="gone"/>
334
+            android:visibility="visible"/>
335
 
335
 
336
 
336
 
337
 
337
 

+ 1 - 0
app/src/main/res/values/strings.xml View File

7
     <string name="github_url_new_issue">https://github.com/yattoz/Tsumugi-app/issues/</string>
7
     <string name="github_url_new_issue">https://github.com/yattoz/Tsumugi-app/issues/</string>
8
     <string name="website_url">https://tsumugi.forum-thalie.fr/</string>
8
     <string name="website_url">https://tsumugi.forum-thalie.fr/</string>
9
     <string name="rss_url">https://tsumugi.forum-thalie.fr/?feed=rss2</string>
9
     <string name="rss_url">https://tsumugi.forum-thalie.fr/?feed=rss2</string>
10
+    <string name="API_URL">https://radio.mahoro-net.org/airtime/api/live-info-v2</string>
10
     <string name="planning_url">ADD SOME URL HERE</string>
11
     <string name="planning_url">ADD SOME URL HERE</string>
11
 
12
 
12
 
13